diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 83e7cc39e..525fa03be 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -1,5 +1,6 @@ aac accessmodes +ACL addon addons adlib @@ -32,6 +33,7 @@ bitrateh bitratel boing boolean +bosch boxofficemojo bpjm britbox @@ -62,6 +64,7 @@ césar d'or dbader de +death deathday deneuve desc @@ -69,6 +72,7 @@ deva diiivoy diiivoycolor dir +dicussed diskstation disneytoon dk @@ -103,6 +107,7 @@ fribb fsk galician generalizable +GID github githubusercontent gitpython @@ -147,6 +152,7 @@ jinja jkirkcaldy johnfawkes jonnywong +joost jpeg jpg json @@ -214,6 +220,7 @@ mza namespace nas natively +ncis nfr nitsua nl @@ -235,6 +242,7 @@ oscar osx ozzy palme +paradise pathing pathvalidate pca @@ -268,6 +276,7 @@ radarr rainbowindent razzie razzies +reacher readme readwriteonce realtime @@ -288,6 +297,7 @@ runtimeh runtimem runtimes rw +sandler schemas sci-fi scifi @@ -302,10 +312,13 @@ skywalker sohjiro sohjiro's sonarr +spartacus ssl stevenlu stevenlu's +stonecutter subprocess +sudo sundance sv sxxeyy @@ -322,6 +335,7 @@ th themoviedb theposterdb thetvdb +tippy tmdb tmdb's tmdbapi @@ -331,6 +345,7 @@ toc todo tomatoesaudience tomatometer +tooltip tooltips trakt trakt's @@ -338,14 +353,19 @@ transcoded transcoding trash truehd +TrueNAS ttf +tubi tutin tv tvdb tw +TZ ua ui +UID uncompress +unconfigured unionfs unmanaged unplayed @@ -354,7 +374,9 @@ uri url urls userlist +uses utills +ValueError vecteezy venv visualstudio @@ -387,4 +409,5 @@ yozoraxcii yyyy zap zeroqi -zh \ No newline at end of file +ziva +zh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5cd934b02..3c80fa310 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -32,10 +32,10 @@ _Please replace this line with a meaningful description of your PR. What does it - Related Issue # - Closes # @@ -57,7 +57,7 @@ _Please replace this line with a meaningful description of your PR. What does it Any PR that goes beyond a minor tweak (i.e. replacing a value, fixing a typo) should have a CHANGELOG entry. We may ask you to update the CHANGELOG if you haven't done so. Type X in the brackets of any relevant option, for example: - - [X] Ye + - [X] Yes --> - [ ] Yes - [ ] No diff --git a/.github/workflows/copyright.yml b/.github/workflows/copyright.yml index fe675ab65..48609b55b 100644 --- a/.github/workflows/copyright.yml +++ b/.github/workflows/copyright.yml @@ -15,14 +15,14 @@ jobs: run: echo "year=$(date +'%Y')" >> $GITHUB_OUTPUT - name: Create App Token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.app-token.outputs.token }} ref: nightly @@ -50,8 +50,8 @@ jobs: with: webhook_id_token: ${{ secrets.BUILD_WEBHOOK }} message: ${{ vars.BUILD_FAILURE_ROLE }} - title: "${{ vars.REPO_NAME }}: ${{ vars.TEXT_COPYRIGHT }}" - url: https://github.com/Kometa-Team/${{ vars.REPO_NAME }}/actions/runs/${{ github.run_id }} + title: "${{ vars.NAME }}: ${{ vars.TEXT_COPYRIGHT }}" + url: https://github.com/Kometa-Team/${{ vars.NAME }}/actions/runs/${{ github.run_id }} color: ${{ vars.COLOR_FAILURE }} username: ${{ vars.BOT_NAME }} avatar_url: ${{ vars.BOT_IMAGE }} diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 15818851a..3260b20b7 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: develop diff --git a/.github/workflows/docker-latest.yml b/.github/workflows/docker-latest.yml index 84e27a4d7..04a3ee7e5 100644 --- a/.github/workflows/docker-latest.yml +++ b/.github/workflows/docker-latest.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/.github/workflows/docker-nightly.yml b/.github/workflows/docker-nightly.yml index 4a17c999a..86f1add90 100644 --- a/.github/workflows/docker-nightly.yml +++ b/.github/workflows/docker-nightly.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: nightly diff --git a/.github/workflows/docker-version.yml b/.github/workflows/docker-version.yml index 66cbcbd3b..29e89ed7c 100644 --- a/.github/workflows/docker-version.yml +++ b/.github/workflows/docker-version.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -76,7 +76,7 @@ jobs: author_icon_url: ${{ vars.DOCKER_IMAGE }} - name: Checkout Configs Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: Kometa-Team/Community-Configs token: ${{ secrets.PAT }} diff --git a/.github/workflows/increment-build.yml b/.github/workflows/increment-build.yml index b50113912..05e4af7a8 100644 --- a/.github/workflows/increment-build.yml +++ b/.github/workflows/increment-build.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: nightly @@ -50,14 +50,14 @@ jobs: steps: - name: Create App Token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.app-token.outputs.token }} ref: nightly @@ -113,7 +113,7 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: nightly diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index d74da2d93..954fd6745 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -13,14 +13,14 @@ jobs: steps: - name: Create App Token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.app-token.outputs.token }} ref: develop diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index aac75f3b9..15b662281 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -18,14 +18,14 @@ jobs: steps: - name: Create App Token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ steps.app-token.outputs.token }} ref: nightly diff --git a/.github/workflows/tag-new-version.yml b/.github/workflows/tag-new-version.yml index 1bb34fe10..72350fb88 100644 --- a/.github/workflows/tag-new-version.yml +++ b/.github/workflows/tag-new-version.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ secrets.PAT }} fetch-depth: 2 diff --git a/.github/workflows/validate-pull.yml b/.github/workflows/validate-pull.yml index 7bcedf884..e2a6714ce 100644 --- a/.github/workflows/validate-pull.yml +++ b/.github/workflows/validate-pull.yml @@ -26,7 +26,7 @@ jobs: exit 1 - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.ref }} @@ -65,7 +65,7 @@ jobs: - name: Run Spellcheck on changed files id: spellcheck - uses: rojopolis/spellcheck-github-actions@0.47.0 + uses: rojopolis/spellcheck-github-actions@0.51.0 with: task_name: Markdown source_files: ${{ steps.changed_files.outputs.all_changed_files }} @@ -83,14 +83,14 @@ jobs: steps: - name: Create App Token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_TOKEN }} - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: token: ${{ secrets.PAT }} ref: ${{ github.event.pull_request.head.ref }} diff --git a/CHANGELOG b/CHANGELOG index 37d963a55..1089502ee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,100 +1,42 @@ # Requirements Update (requirements will need to be reinstalled) -Add pywin32 requirement at 308 (windows only) -Add cloudscraper requirement at 1.2.71 -Update gitpython requirement to 3.1.44 -Update lxml requirement to 5.3.1 -Update num2words requirement to 0.5.14 -Update pathvalidate requirement to 3.2.3 -Update pillow requirement to 11.1.0 -Update plexapi requirement to 4.16.1 -Update psutil requirement to 7.0.0 -Update python-dotenv requirement to 1.1.0 -Update pywin32 requirement to 310 -Update ruamel-yaml requirement to 0.18.9 -Update setuptools requirement to 78.1.0 -Update tmdbapis requirement to 1.2.28 +Update gitpython requirement to 3.1.45 +Update lxml requirement to 6.0.0 +Update pathvalidate requirement to 3.3.1 +Update pillow requirement to 11.3.0 +Update plexapi requirement to 4.17.0 +Update python-dotenv requirement to 1.1.1 +Update pywin32 requirement to 311 +Update requests requirement to 2.32.4 +Update ruamel-yaml requirement to 0.18.14 +Update setuptools requirement to 80.9.0 +Update tenacity requirement to 9.1.2 # Important Changes -Python 3.8 is no longer supported. The minimum version of Python required is now 3.9. # New Features -Added the `character` search option to the `imdb_search` builder -Added ability to use Show-level ratings at the season and episode level for Overlays if the original source does not provide ratings at the season or episode level. This is accomplished using (Special Text Variables)[https://kometa.wiki/en/latest/files/overlays/#special-text-variables] but is not yet available for the `Ratings` Defaults file. -Add `show_unfiltered` setting to display items which make it through a filter -Allow `sync_to_trakt_list` on episode-level collections -Logic added to Kometa to create `config.yml` if it does not exist from the `config.yml.template` file. If the template file cannot be found, Kometa will attempt to download it from GitHub. -When using `mass_poster_update`, added `ignore_locked` and `ignore_overlays` attributes which will prevent Kometa from resetting the image if the poster field is locked (i.e. a previous mass poster update) or if the item has an Overlay. This can effectively act as a differential update system. -When using `mass_background_update`, added `ignore_locked` attribute which will prevent Kometa from resetting the image if the poster field is locked (i.e. a previous mass poster update). This can effectively act as a differential update system. -Added `date` option for schedules -Added the `--low-priority/-lp` (`KOMETA_LOW_PRIORITY`) command line argument option to run the process at a lower priority. **credit to @planetrocky** -Added `trakt`, `omdb_metascore`, `omdb_tomatoes` ratings sources for mass operations. -Added `trakt` ratings source for mass episode operations. -Added GitHub token validation during config validation. -Added `plex` ratings source for mass operations. -Add recently-added ratings sources to special-text overlays. -Added IMDb Interests (sub-genres) to `imdb_search` builder -Allow `server_preroll` to accept a list -Changed default `overlay_artwork_filetype` to `webp_lossy` and `overlay_artwork_quality` to 90 -Added `ntfy` as a notification option -Added `scale_width` and `scale_height` overlay options. -Added `tmdb_deathday` **credit to @Kevin2kkelly** +Added `file_logo` and `url_logo` as metadata image options **credit to @Joost1991** +Added support for updating logos via the asset directories as well **credit to @Joost1991** +Added support for `dovi` search attribute for Plex builders (requires PMS 1.41.6.9606 or above) +IMDb episodes found in a list for a show collection will now add that episode's show to the collection +Adds `force_refresh` Trakt setting; this tells Kometa to refresh the token on every run without checking it first +Adds a log of the response from Trakt which triggers the "No Valid Lists" error. +Adds `assets_for_all_collections` library operation; this tells Kometa to add asset artwork to unmanaged and unconfigured collections # Docs -Added "getting started" page -Added page to describe all the YAML files -Updated Synology page for DSM 7.2 and added a disclaimer about what it covers -Added "undoing changes" page -Added more detail to the "sorting" page +Adds overlay guide +Adds TrueNAS walkthrough +Adds notes on Github tokens +Adds screenshot of UNRaid Console menu # Defaults -Fixed incorrect content rating mappings in various Default files -Fixes an issue where Prime Video overlays/collections would not be built when the `watch_region` is set to AU or NL -Fixes an issue where Rotten Tomatoes Verified Hot wasn't working -Updates `Alien vs Predator` and `X-Men` lists to new lists which include most recent releases -Adds `style` template variable for Streaming and Chart defaults, allowing user to choose color or white logos for collection posters -Added `Paramount+ with Showtime` to both `Paramount+` and `Showtime` for Networks and Streaming, any existing weighting is unchanged. -Added Aymara language with Bolivian flag to audio/subtitle overlay languages (credit to popeadam) -Added `size` setting to languages overlay to double the overlay size. -Added `hide_text` setting to languages overlay to only show the flags and hide the country text. -Added `openmatte` edition to default resolution overlay. -Added `Metacritic Must See Movies` to `other_chart`. -Moved several `universe` and `playlist` default lists away from Trakt and over to IMDb and MDBList -Removed default emojis from `seasonal` due to Plex issue with emojis and updated some lists -Removed BritBox and replaced with ITVX in `streaming` following service shutdown +[Franchise] Updates 'NCIS' list to include NCIS: Origins and NCIS: Tony & Ziva. Adds 'Spartacus', 'Death in Paradise' and 'Reacher' to the franchise lists +[Universe] Change the DC Animated Universe list to a mdblist # Bug Fixes -Fixed the `cast` search option for the `imdb_search` builder -Fixes #2258 `imdb_list` sort was not being parsed correctly -Fixes `letterboxd_list` rating filter to use a 1-10 rating vs 1-100 to reflect how letterboxd ratings work on their website -Fixes #2274 Enhance handling of smart collections in deletion -Fixed the `ids_to_anidb` lookup for anime movies and shows -Fixes an issue where episode overlays sometimes wouldn't be added -Fixes an issue with IMDb Parental Labels not working -Fixes an issue where OMDb returned `N/A` as the content rating -Fixes an issue where `plex_collectionless` doesn't work if the item was added to a collection in the same run -Adds a page that discusses the different YAML files and what they do. -Modifies default value presentation for default metadata files. -Fixes an issue causing IMDB collection to fail due to duplicate keys -Removed Blog from the Navigation due to lack of time for updating/maintaining it -Fixes #2354 by updating version of tmdbapi dependency -Added Start Time, Finished and Run Time to Summary of run. -Fixed an issue where custom repositories would not work correctly if the URL did not end in a trailing `/` character. -Kometa will now check for `.yaml` extension in filenames if `.yml` is not found and vice-versa -Log files will now follow the naming convention of `kometa.log`, `kometa-1.log` (previous run), `kometa-2.log (2 runs ago) etc. -Kometa will no longer automatically sync playlists to all users if you do not specify who you want to sync them to. Only the server admin will receive playlists unless otherwise specified using `sync_to_users` or `playlist_sync_to_users` -Fixes #2385 `tmdb_person` would pass an integer if the name started with an integer (i.e. `50 Cent` would pass `50` which resolved to `Catherine Deneuve`) -Fixes an issue where `show_missing` would display missing movies against show libraries (closes #2351) -Fixed an OMDb API issue where API key would intermittently be treated as invalid -Fixed an issue where Kometa would try to upload and cache images larger than Plex allows (10mb is the upper limit) -Fixes an issue where `use_subtitles` would ignore `flag_alignment: left` -Fixed typo `radarr_tag_list` instead of `radarr_taglist` in `builder` module causing `Collection Error: radarr_taglist attribute is blank` -Fixed NoneError when using a blank `radarr_taglist` or `sonarr_taglist`. -Fixes an issue with boolean filter matching. -Fixes an issue where the decade default collection names were incorrect. -Fixes the playlist default to automatically work with a supplied list. -Remove an unnecessary request to Plex while processing overlays. -Fixes issue with ICheckMovies parsing. -Fixes issue with Letterboxd parsing. -Fixes issue with the `mojo_domestic` BoxOfficeMojo Builder. -Fixes an issue updating Trakt User Ratings when the show doesn't exist on TVDb. -Fixes #2560 count display issue \ No newline at end of file + +Fixed an issue where Playlists could not add to both Radarr and Sonarr in a single run +Fixed an issue where `trakt_list_details` would not find a list's summary if it was an "official" Trakt list +Fixed an issue where log file paths with >2 periods would cause a ValueError +Fixed an issue with IMDb Awards +Restored image language settings after change in base image +Fixed an issue with IMDb List and Watchlist diff --git a/Dockerfile b/Dockerfile index bbe2af0a9..ac6eed502 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ ARG BRANCH_NAME=master ENV BRANCH_NAME=${BRANCH_NAME} ENV TINI_VERSION=v0.19.0 ENV KOMETA_DOCKER=True +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 COPY requirements.txt requirements.txt RUN echo "**** install system packages ****" \ && apt-get update \ diff --git a/README.md b/README.md index 52747b4b9..54ea522b9 100644 --- a/README.md +++ b/README.md @@ -79,19 +79,19 @@ started. These guides will take you through the process of installing Kometa, cr For those who need full installation walkthroughs, please refer to the following walkthrough guides: - * [Local Walkthrough](https://kometa.wiki/en/latest/kometa/install/local/) - follow this if you are running the script directly on Windows, OS X, or Linux. - * [Docker Walkthrough](https://kometa.wiki/en/latest/kometa/install/docker/) - this discusses using Docker at the command line. + * [Local Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/local/) - follow this if you are running the script directly on Windows, OS X, or Linux. + * [Docker Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/docker/) - this discusses using Docker at the command line. If you are using unRAID, Kubernetes, QNAP, or Synology refer to the following basic guide to Docker container setup for each system: **This doesn't cover the Kometa setup specifics found in the guides above with regard to creating the config file and collection file, so you may want to go through the -[Docker Walkthrough](https://kometa.wiki/en/latest/kometa/install/docker/) first on your computer to gain that understanding.** +[Docker Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/docker/) first on your computer to gain that understanding.** + + * [unRAID Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/unraid/) + * [Kubernetes Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/kubernetes/) + * [QNAP Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/qnap/) + * [Synology Walkthrough](https://kometa.wiki/en/latest/kometa/install/walkthroughs/synology/) - * [unRAID Walkthrough](https://kometa.wiki/en/latest/kometa/install/unraid/) - * [Kubernetes Walkthrough](https://kometa.wiki/en/latest/kometa/install/kubernetes/) - * [QNAP Walkthrough](https://kometa.wiki/en/latest/kometa/install/qnap/) - * [Synology Walkthrough](https://kometa.wiki/en/latest/kometa/install/synology/) - ## Example Usage @@ -113,7 +113,7 @@ Collections and Overlays that fit your unique preferences and make discovering y But if you don't want to spend time manually creating Collections and Overlays, we've got you covered. Check out the [Kometa Defaults](https://kometa.wiki/en/latest/defaults/guide/) - a handcrafted selection of tried-and-tested Collections and Overlays made by the Kometa team. - + ## Alternate Branches The Develop and Nightly branches are "beta" versions of Kometa that are updated more frequently than the stable version (Master branch). @@ -219,13 +219,13 @@ discussions relating to changes made in the nightly branch will be held within t If you're looking for support for any questions or issues you might have, or if you just want to be a part of our growing community, Join the [Kometa Discord Server](https://kometa.wiki/en/latest/discord/). - ## Feature Requests At Kometa, we value our community's input and actively seek feedback to drive the evolution of our product. We want to hear your ideas on how to enhance Kometa, and we encourage you to visit our [Feature Request](https://features.kometa.wiki/features) page to share your thoughts or vote on what features you would like to see added next. Your voice matters and helps shape the future of Kometa, so please don't hesitate to join in the conversation and be a part of our community-driven development process. + ## Errors and Configuration Questions If you're having trouble, we recommend first joining the [Kometa Discord Server](https://kometa.wiki/en/latest/discord/) @@ -240,7 +240,7 @@ and seeking support there. If that isn't possible for you, here's what you can d Remember, the community helps shape the future of Kometa, so your input is valuable! For support on any of the above, visit the [Discord server](https://kometa.wiki/en/latest/discord/). - + ## Contributing * Pull Requests are greatly encouraged, please submit all Pull Requests to the nightly branch. - + diff --git a/VERSION b/VERSION index ccbccc3dc..c043eea77 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.0 +2.2.1 diff --git a/config/config.yml.template b/config/config.yml.template index 18ad6d825..24e21653f 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -1,9 +1,12 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kometa-team/kometa/nightly/json-schema/config-schema.json +## Do not remove the above line ## This file is a template remove the .template to use the file ## Plex and TMDb are the two connections which are required for the script to run -plex: # Can be individually specified per library as well; REQUIRED for the script to run - url: http://192.168.1.12:32400 - token: Enter Plex Token + +plex: # Can be individually specified per library as well; REQUIRED for the script to run + url: http://192.168.1.12:32400 # Enter Plex URL (REQUIRED); this needs to point to your server, not app.plex.tv + token: # Enter Plex Token (REQUIRED) timeout: 60 db_cache: 40 clean_bundles: false @@ -11,44 +14,44 @@ plex: # Can be individually specified per library optimize: false verify_ssl: true tmdb: - apikey: Enter TMDb API Key + apikey: # Enter TMDb API Key (REQUIRED) cache_expiration: 60 language: en region: -## At least one library has to be configured for the script to do anything meaningful -libraries: # This is called out once within the config.yml file - Movies: # These are names of libraries in your Plex - remove_overlays: false # Set this to true to remove all overlays + ## At least one library has to be configured for the script to do anything meaningful +libraries: # This is called out once within the config.yml file + Movies: # These are names of libraries in your Plex + remove_overlays: false # Set this to true to remove all overlays collection_files: - - default: basic # This is a file within Kometa's defaults folder - - default: imdb # This is a file within Kometa's defaults folder - # see the wiki for how to use local files, folders, URLs, or files from git + - default: basic # This is a file within Kometa's defaults folder + - default: imdb # This is a file within Kometa's defaults folder + # see the wiki for how to use local files, folders, URLs, or files from git overlay_files: - - default: ribbon # This is a file within Kometa's defaults folder - # see the wiki for how to use local files, folders, URLs, or files from git + - default: ribbon # This is a file within Kometa's defaults folder + # see the wiki for how to use local files, folders, URLs, or files from git TV Shows: - remove_overlays: false # Set this to true to remove all overlays + remove_overlays: false # Set this to true to remove all overlays collection_files: - - default: basic # This is a file within Kometa's defaults folder - - default: imdb # This is a file within Kometa's defaults folder - # see the wiki for how to use local files, folders, URLs, or files from git + - default: basic # This is a file within Kometa's defaults folder + - default: imdb # This is a file within Kometa's defaults folder + # see the wiki for how to use local files, folders, URLs, or files from git overlay_files: - - default: ribbon # This is a file within Kometa's defaults folder - # see the wiki for how to use local files, folders, URLs, or files from git + - default: ribbon # This is a file within Kometa's defaults folder + # see the wiki for how to use local files, folders, URLs, or files from git Anime: collection_files: - - default: basic # This is a file within Kometa's defaults folder - - default: anilist # This is a file within Kometa's defaults folder - # see the wiki for how to use local files, folders, URLs, or files from git + - default: basic # This is a file within Kometa's defaults folder + - default: anilist # This is a file within Kometa's defaults folder + # see the wiki for how to use local files, folders, URLs, or files from git Music: collection_files: - - file: config/Music.yml # This is a local file THAT YOU MIGHT CREATE + - file: config/Music.yml # This is a local file THAT YOU MIGHT CREATE playlist_files: - - default: playlist # This is a file within Kometa's defaults folder + - default: playlist # This is a file within Kometa's defaults folder template_variables: - libraries: Movies, TV Shows # list of libraries that you want the Kometa Defaults playlists to look at - # see the wiki for how to use local files, folders, URLs, or files from git + libraries: Movies, TV Shows # list of libraries that you want the Kometa Defaults playlists to look at + # see the wiki for how to use local files, folders, URLs, or files from git settings: run_order: - operations @@ -95,81 +98,81 @@ settings: custom_repo: overlay_artwork_filetype: webp_lossy overlay_artwork_quality: 90 -webhooks: # Can be individually specified per library as well +webhooks: # Can be individually specified per library as well changes: delete: error: run_end: run_start: version: -tautulli: # Can be individually specified per library as well - url: http://192.168.1.12:8181 - apikey: Enter Tautulli API Key +tautulli: # Can be individually specified per library as well + url: http://192.168.1.12:8181 # Enter Tautulli URL (Optional) + apikey: # Enter Tautulli API Key (Optional) github: - token: Enter GitHub Personal Access Token + token: # Enter GitHub Personal Access Token (Optional) omdb: - apikey: Enter OMDb API Key + apikey: # Enter OMDb API Key (Optional) cache_expiration: 60 mdblist: - apikey: Enter MDBList API Key + apikey: # Enter MDBList API Key (Optional) cache_expiration: 60 notifiarr: - apikey: Enter Notifiarr API Key + apikey: # Enter Notifiarr API Key (Optional) gotify: - url: http://192.168.1.12:80 - token: Enter Gotify Token + url: http://192.168.1.12:80 # Enter Gotify server URL (Optional) + token: # Enter Gotify Token (Optional) ntfy: - url: http://192.168.1.12:80 - token: Enter ntfy Access Token - topic: Enter ntfy Topic -anidb: # Not required for AniDB builders unless you want mature content - username: Enter AniDB Username - password: Enter AniDB Password + url: http://192.168.1.12:80 # Enter ntfy server URL (Optional) + token: # Enter ntfy Access Token (Optional) + topic: # Enter ntfy Topic (Optional) +anidb: # Not required for AniDB builders unless you want mature content + username: # Enter AniDB Username (Optional) + password: # Enter AniDB Password (Optional) cache_expiration: 60 - client: Enter AniDB Client + client: # Enter AniDB Client (Optional) language: en version: 1 -radarr: # Can be individually specified per library as well - url: http://192.168.1.12:7878 - token: Enter Radarr API Key +radarr: # Can be individually specified per library as well + url: http://192.168.1.12:7878 # Enter Radarr server URL (Optional) + token: # Enter Radarr API Key (Optional) add_missing: false add_existing: false upgrade_existing: false monitor_existing: false - root_folder_path: "S:/Movies" + root_folder_path: "S:/Movies" # This value is CASE SENSITIVE monitor: true availability: announced - quality_profile: HD-1080p + quality_profile: HD-1080p # This value is CASE SENSITIVE tag: search: false - radarr_path: - plex_path: + radarr_path: # This value is only needed in combination with the *_existing settings + plex_path: # This value is only needed in combination with the *_existing settings ignore_cache: false -sonarr: # Can be individually specified per library as well - url: http://192.168.1.12:8989 - token: Enter Sonarr API Key +sonarr: # Can be individually specified per library as well + url: http://192.168.1.12:8989 # Enter Sonarr server URL (Optional) + token: # Enter Sonarr API Key (Optional) add_missing: false add_existing: false upgrade_existing: false monitor_existing: false - root_folder_path: "S:/TV Shows" + root_folder_path: "S:/TV Shows" # This value is CASE SENSITIVE monitor: all - quality_profile: HD-1080p + quality_profile: HD-1080p # This value is CASE SENSITIVE language_profile: English series_type: standard season_folder: true tag: search: false cutoff_search: false - sonarr_path: - plex_path: + sonarr_path: # This value is only needed in combination with the *_existing settings + plex_path: # This value is only needed in combination with the *_existing settings ignore_cache: false trakt: - client_id: Enter Trakt Client ID - client_secret: Enter Trakt Client Secret + client_id: # Enter Trakt Client ID (Optional) + client_secret: # Enter Trakt Client Secret (Optional) pin: authorization: - # authorization section is autofilled by the script + # authorization section is autofilled by the script access_token: created_at: expires_in: @@ -177,12 +180,12 @@ trakt: scope: public token_type: mal: - client_id: Enter MyAnimeList Client ID - client_secret: Enter MyAnimeList Client Secret + client_id: # Enter MyAnimeList Client ID (Optional) + client_secret: # Enter MyAnimeList Client Secret (Optional) cache_expiration: 60 localhost_url: authorization: - # authorization section is autofilled by the script + # authorization section is autofilled by the script access_token: expires_in: refresh_token: diff --git a/defaults/award/golden.yml b/defaults/award/golden.yml index e398aae59..4b67b63a6 100644 --- a/defaults/award/golden.yml +++ b/defaults/award/golden.yml @@ -36,10 +36,7 @@ collections: - best motion picture - musical - best motion picture - musical or comedy - best motion picture - non-english language - - best motion picture, animated - - best motion picture, drama - best motion picture, musical or comedy - - best motion picture, non-english language winning: true Golden Globes Best Director Winners: @@ -59,7 +56,6 @@ collections: category_filter: - best director - best director - motion picture - - best director, motion picture winning: true dynamic_collections: diff --git a/defaults/both/content_rating_de.yml b/defaults/both/content_rating_de.yml index 2e04a992d..7fa4033ff 100644 --- a/defaults/both/content_rating_de.yml +++ b/defaults/both/content_rating_de.yml @@ -16,6 +16,7 @@ collections: - name: separator separator: content_rating key_name: Ratings + sort_title: <><>_0-Ratings translation_key: separator dynamic_collections: @@ -74,6 +75,7 @@ dynamic_collections: - no/A - no/5 - no/05 + - FSK-0 6: - de/6 - gb/9+ @@ -98,6 +100,7 @@ dynamic_collections: - no/09 - no/10 - no/11 + - FSK-6 12: - de/12 - gb/12 @@ -111,6 +114,7 @@ dynamic_collections: - PG-13 - Teens 13 or older - PG-13 - no/15 + - FSK-12 16: - de/16 - no/16 @@ -120,6 +124,7 @@ dynamic_collections: - R - 17 - M/PG + - FSK-16 18: - de/18 - gb/18 @@ -132,6 +137,7 @@ dynamic_collections: - NC-17 - R+ - Mild Nudity - Rx - Hentai + - FSK-18 BPjM: - de/BPjM Restricted - BPjM Restricted diff --git a/defaults/both/streaming.yml b/defaults/both/streaming.yml index eb5985d40..61307c4d5 100644 --- a/defaults/both/streaming.yml +++ b/defaults/both/streaming.yml @@ -111,7 +111,7 @@ dynamic_collections: discovery: discovery+ disney: Disney+ itvx: ITVX - max: Max + hbomax: HBO Max hayu: hayu hulu: Hulu netflix: Netflix @@ -120,6 +120,7 @@ dynamic_collections: peacock: Peacock amazon: Prime Video youtube: YouTube + tubi: tubi title_format: <> <>s template: - streaming @@ -136,10 +137,10 @@ dynamic_collections: crunchyroll: 283 discovery: 510 disney: 337 - itvx: 41|2300 - max: 1899 + hbomax: 1899 hayu: 223 hulu: 15 + itvx: 41|2300 netflix: 8 now: 39 paramount: 531|1770 @@ -147,6 +148,7 @@ dynamic_collections: amazon: 9 showtime: 37 youtube: 188 + tubi: 73 originals_only: default: false allowed_libraries: @@ -169,3 +171,6 @@ dynamic_collections: itvx: - BritBox Movies - BritBox Shows + hbomax: + - Max Movies + - Max Shows diff --git a/defaults/both/studio.yml b/defaults/both/studio.yml index 9a2d9a12b..646a54c95 100644 --- a/defaults/both/studio.yml +++ b/defaults/both/studio.yml @@ -718,6 +718,7 @@ dynamic_collections: - Disney Warner Animation Group: - Warner Bros. Cartoon Studios + - Warner Bros. Cartoons - Warner Animation Warner Bros. Pictures: - Warner diff --git a/defaults/both/universe.yml b/defaults/both/universe.yml index 836eaa809..63b80c0d5 100644 --- a/defaults/both/universe.yml +++ b/defaults/both/universe.yml @@ -70,6 +70,7 @@ dynamic_collections: avp: Alien / Predator arrow: Arrowverse askew: View Askewniverse + conjuring: Conjuring Universe dca: DC Animated Universe dcu: DC Universe fast: Fast & Furious @@ -79,7 +80,7 @@ dynamic_collections: rocky: Rocky / Creed trek: Star Trek star: Star Wars Universe - mummy: The Mummy Universe + mummy: Mummy Universe wizard: Wizarding World xmen: X-Men Universe template: @@ -90,6 +91,7 @@ dynamic_collections: template_variables: allowed_libraries: avp: movie + conjuring: movie wizard: movie fast: movie rocky: movie @@ -97,13 +99,14 @@ dynamic_collections: imdb_url: arrow: https://www.imdb.com/list/ls566667558/ avp: https://www.imdb.com/list/ls543971628/ - dca: https://www.imdb.com/list/ls590606031/ dcu: https://www.imdb.com/list/ls524274984/ mcu: https://www.imdb.com/list/ls539646485/ star: https://www.imdb.com/list/ls501373412/ trek: https://www.imdb.com/list/ls547463722/ xmen: https://www.imdb.com/list/ls567618635/ mdblist_url: + conjuring: https://mdblist.com/lists/exoduso/the-conjuring-universe-4i5i1p4qag + dca: https://mdblist.com/lists/johnfawkes/dca fast: https://mdblist.com/lists/k0meta/external/9246 marvel: https://mdblist.com/lists/k0meta/external/15110 middle: https://mdblist.com/lists/k0meta/external/46550 @@ -114,4 +117,6 @@ dynamic_collections: image: default: universe/<> delete_collections_named: - dcu: DC Extended Universe \ No newline at end of file + dcu: DC Extended Universe + mummy: The Mummy Universe + conjuring: The Conjuring Universe diff --git a/defaults/movie/composer.yml b/defaults/movie/composer.yml new file mode 100644 index 000000000..9406eaabe --- /dev/null +++ b/defaults/movie/composer.yml @@ -0,0 +1,46 @@ +############################################################################## +# Director Collections # +# Created by Yozora, Bullmoose20, & Sohjiro # +# EDITING THIS FILE MAY CAUSE PULLING NEW UPDATES TO FAIL # +# https://kometa.wiki/en/latest/defaults/movie/director # +############################################################################## + +external_templates: + default: templates + template_variables: + collection_section: "150" + +collections: + Composers Collections: + template: + - name: separator + separator: composer + key_name: Composers + translation_key: separator + +dynamic_collections: + Top Composers: + type: composer + data: + depth: 5 + limit: 25 + title_format: <> (Composer) + template: + - tmdb_person + - smart_filter + - shared + template_variables: + tmdb_person: + default: <> + tmdb_person_offset: + default: 0 + search_term: + default: composer + search_value: + default: tmdb + translation_key: + default: composer + style: + default: bw + dynamic: + default: true diff --git a/defaults/movie/seasonal.yml b/defaults/movie/seasonal.yml index 77ca31b3d..4412ee27e 100644 --- a/defaults/movie/seasonal.yml +++ b/defaults/movie/seasonal.yml @@ -258,8 +258,8 @@ dynamic_collections: christmas: 🎅 Christmas Movies aapi: 🌊🌺 Asian American Pacific Islander Movies disabilities: ♿ Disability Month Movies - black_history: ✊ 🏿 Black History Month Movies - lgbtq: L🏳️‍🌈 LGBTQ Month Movies + black_history: ✊🏿 Black History Month Movies + lgbtq: 🏳️‍🌈 LGBTQ Month Movies latinx: 🪅 National Hispanic Heritage Movies women: 🚺 Women's History Month Movies visible_home: diff --git a/defaults/overlays/content_rating_nz.yml b/defaults/overlays/content_rating_nz.yml index 633c93217..7ec1571d9 100644 --- a/defaults/overlays/content_rating_nz.yml +++ b/defaults/overlays/content_rating_nz.yml @@ -59,49 +59,49 @@ overlays: m: template: - name: standard - - {name: cr_nz, rating: "au/M, de/12, gb/12, 12, no/12, gb/15, gb/14+, TV-14, 12, 13, 14, 15, PG-13 - Teens 13 or older, PG-13, no/15"} - + - {name: cr_nz, rating: "au/M, de/12, gb/12, 12, no/12, 12"} + r13: template: - name: standard - - {name: cr_nz, rating: "au/M, de/12, gb/12, 12, no/12, gb/15, gb/14+, TV-14, 12, 13, 14, 15, PG-13 - Teens 13 or older, PG-13, no/15"} + - {name: cr_nz, rating: "13, PG-13 - Teens 13 or older, PG-13"} rp13: template: - name: standard - - {name: cr_nz, rating: "au/M, de/12, gb/12, 12, no/12, gb/15, gb/14+, TV-14, 12, 13, 14, 15, PG-13 - Teens 13 or older, PG-13, no/15"} + - {name: cr_nz, rating: "gb/14+, TV-14, 14"} r15: template: - name: standard - - {name: cr_nz, rating: "au/MA15+, de/16, no/16, A-17, TVMA, TV-MA, R, 16, 17, M/PG"} + - {name: cr_nz, rating: "au/MA15+, 15, M, TVMA, TV-MA"} r16: template: - name: standard - - {name: cr_nz, rating: "au/MA15+, de/16, no/16, A-17, TVMA, TV-MA, R, 16, 17, M/PG"} + - {name: cr_nz, rating: "de/16, no/16, 16"} rp16: template: - name: standard - - {name: cr_nz, rating: "au/MA15+, de/16, no/16, A-17, TVMA, TV-MA, R, 16, 17, M/PG"} + - {name: cr_nz, rating: "A-17, 17"} r: template: - name: standard - - {name: cr_nz, rating: "au/R 18+, de/18, gb/18, M, 18, R - 17+ (violence & profanity), no/18, R18, gb/R18, gb/X, X, NC-17, R+ - Mild Nudity, Rx - Hentai"} + - {name: cr_nz, rating: "18, R, R - 17+ (violence & profanity), no/18, NC-17, R+ - Mild Nudity, Rx - Hentai"} r18: template: - name: standard - - {name: cr_nz, rating: "au/X 18+, de/BPjM Restricted, BPjM Restricted"} + - {name: cr_nz, rating: "de/18, gb/18, gb/R18, R18"} rp18: template: - name: standard - - {name: cr_nz, rating: "au/X 18+, de/BPjM Restricted, BPjM Restricted"} + - {name: cr_nz, rating: "au/X 18+, gb/X, X, de/BPjM Restricted, BPjM Restricted"} nr: template: - {name: standard, key: nr} - - {name: cr_nz, rating: "None, NR, Not Rated, Unrated, de/Unrated, de/Not Rated, au/Unrated, au/Not Rated"} \ No newline at end of file + - {name: cr_nz, rating: "None, NR, Not Rated, Unrated, de/Unrated, de/Not Rated, au/Unrated, au/Not Rated"} diff --git a/defaults/overlays/images/cr/de0.png b/defaults/overlays/images/cr/de0.png index 3df8da07d..61cdcc39e 100644 Binary files a/defaults/overlays/images/cr/de0.png and b/defaults/overlays/images/cr/de0.png differ diff --git a/defaults/overlays/images/cr/de0c.png b/defaults/overlays/images/cr/de0c.png index 3df8da07d..01addd7c9 100644 Binary files a/defaults/overlays/images/cr/de0c.png and b/defaults/overlays/images/cr/de0c.png differ diff --git a/defaults/overlays/images/cr/de12.png b/defaults/overlays/images/cr/de12.png index 1b7e0b821..64c7029b4 100644 Binary files a/defaults/overlays/images/cr/de12.png and b/defaults/overlays/images/cr/de12.png differ diff --git a/defaults/overlays/images/cr/de12c.png b/defaults/overlays/images/cr/de12c.png index 482a6073b..88121ccf9 100644 Binary files a/defaults/overlays/images/cr/de12c.png and b/defaults/overlays/images/cr/de12c.png differ diff --git a/defaults/overlays/images/cr/de16.png b/defaults/overlays/images/cr/de16.png index dcaee1a32..7c0ade018 100644 Binary files a/defaults/overlays/images/cr/de16.png and b/defaults/overlays/images/cr/de16.png differ diff --git a/defaults/overlays/images/cr/de16c.png b/defaults/overlays/images/cr/de16c.png index 64544ce43..ca226c886 100644 Binary files a/defaults/overlays/images/cr/de16c.png and b/defaults/overlays/images/cr/de16c.png differ diff --git a/defaults/overlays/images/cr/de18.png b/defaults/overlays/images/cr/de18.png index 5d24b2cf6..008c5ed7b 100644 Binary files a/defaults/overlays/images/cr/de18.png and b/defaults/overlays/images/cr/de18.png differ diff --git a/defaults/overlays/images/cr/de18c.png b/defaults/overlays/images/cr/de18c.png index 65a1595f3..0af79477b 100644 Binary files a/defaults/overlays/images/cr/de18c.png and b/defaults/overlays/images/cr/de18c.png differ diff --git a/defaults/overlays/images/cr/de6.png b/defaults/overlays/images/cr/de6.png index a558e1db3..36daa099c 100644 Binary files a/defaults/overlays/images/cr/de6.png and b/defaults/overlays/images/cr/de6.png differ diff --git a/defaults/overlays/images/cr/de6c.png b/defaults/overlays/images/cr/de6c.png index 09a9458b8..2cad5bc19 100644 Binary files a/defaults/overlays/images/cr/de6c.png and b/defaults/overlays/images/cr/de6c.png differ diff --git a/defaults/overlays/images/cr/debpjm.png b/defaults/overlays/images/cr/debpjm.png index d501ca61f..785460846 100644 Binary files a/defaults/overlays/images/cr/debpjm.png and b/defaults/overlays/images/cr/debpjm.png differ diff --git a/defaults/overlays/images/cr/debpjmc.png b/defaults/overlays/images/cr/debpjmc.png index a040764d3..a62c87af4 100644 Binary files a/defaults/overlays/images/cr/debpjmc.png and b/defaults/overlays/images/cr/debpjmc.png differ diff --git a/defaults/overlays/images/network/color/tubi.png b/defaults/overlays/images/network/color/tubi.png new file mode 100644 index 000000000..4b4ec6066 Binary files /dev/null and b/defaults/overlays/images/network/color/tubi.png differ diff --git a/defaults/overlays/images/network/white/tubi.png b/defaults/overlays/images/network/white/tubi.png new file mode 100644 index 000000000..5f179b24c Binary files /dev/null and b/defaults/overlays/images/network/white/tubi.png differ diff --git a/defaults/overlays/images/rating/Letterboxd.png b/defaults/overlays/images/rating/Letterboxd.png index 230ef90c3..fe7e1cb6c 100644 Binary files a/defaults/overlays/images/rating/Letterboxd.png and b/defaults/overlays/images/rating/Letterboxd.png differ diff --git a/defaults/overlays/images/rating/MetacriticTop.png b/defaults/overlays/images/rating/MetacriticTop.png index 095ee2381..23c6050e6 100644 Binary files a/defaults/overlays/images/rating/MetacriticTop.png and b/defaults/overlays/images/rating/MetacriticTop.png differ diff --git a/defaults/overlays/images/rating/MetacriticTop.psd b/defaults/overlays/images/rating/MetacriticTop.psd new file mode 100644 index 000000000..6ea67185d Binary files /dev/null and b/defaults/overlays/images/rating/MetacriticTop.psd differ diff --git a/defaults/overlays/images/ribbon/mixed/bafta.png b/defaults/overlays/images/ribbon/mixed/bafta.png new file mode 100644 index 000000000..376e26b5b Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/bafta.png differ diff --git a/defaults/overlays/images/ribbon/mixed/berlinale.png b/defaults/overlays/images/ribbon/mixed/berlinale.png new file mode 100644 index 000000000..67303d3a9 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/berlinale.png differ diff --git a/defaults/overlays/images/ribbon/mixed/blank-mixed.png b/defaults/overlays/images/ribbon/mixed/blank-mixed.png new file mode 100644 index 000000000..f6e30c1c1 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/blank-mixed.png differ diff --git a/defaults/overlays/images/ribbon/mixed/cannes.png b/defaults/overlays/images/ribbon/mixed/cannes.png new file mode 100644 index 000000000..8e44ecc66 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/cannes.png differ diff --git a/defaults/overlays/images/ribbon/mixed/cesar.png b/defaults/overlays/images/ribbon/mixed/cesar.png new file mode 100644 index 000000000..da7b1edf9 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/cesar.png differ diff --git a/defaults/overlays/images/ribbon/mixed/choice.png b/defaults/overlays/images/ribbon/mixed/choice.png new file mode 100644 index 000000000..07af5b774 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/choice.png differ diff --git a/defaults/overlays/images/ribbon/mixed/common.png b/defaults/overlays/images/ribbon/mixed/common.png new file mode 100644 index 000000000..85fd70e7e Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/common.png differ diff --git a/defaults/overlays/images/ribbon/mixed/emmys.png b/defaults/overlays/images/ribbon/mixed/emmys.png new file mode 100644 index 000000000..1e36f0a91 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/emmys.png differ diff --git a/defaults/overlays/images/ribbon/mixed/golden.png b/defaults/overlays/images/ribbon/mixed/golden.png new file mode 100644 index 000000000..c5725daed Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/golden.png differ diff --git a/defaults/overlays/images/ribbon/mixed/golden_director.png b/defaults/overlays/images/ribbon/mixed/golden_director.png new file mode 100644 index 000000000..57735d2e0 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/golden_director.png differ diff --git a/defaults/overlays/images/ribbon/mixed/imdb.png b/defaults/overlays/images/ribbon/mixed/imdb.png new file mode 100644 index 000000000..8b52ea890 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/imdb.png differ diff --git a/defaults/overlays/images/ribbon/mixed/letterboxd.png b/defaults/overlays/images/ribbon/mixed/letterboxd.png new file mode 100644 index 000000000..99e93c3df Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/letterboxd.png differ diff --git a/defaults/overlays/images/ribbon/mixed/metacritic.png b/defaults/overlays/images/ribbon/mixed/metacritic.png new file mode 100644 index 000000000..10ed929de Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/metacritic.png differ diff --git a/defaults/overlays/images/ribbon/mixed/netflix.png b/defaults/overlays/images/ribbon/mixed/netflix.png new file mode 100644 index 000000000..a153b0278 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/netflix.png differ diff --git a/defaults/overlays/images/ribbon/mixed/oscars.png b/defaults/overlays/images/ribbon/mixed/oscars.png new file mode 100644 index 000000000..7cf57a906 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/oscars.png differ diff --git a/defaults/overlays/images/ribbon/mixed/oscars_director.png b/defaults/overlays/images/ribbon/mixed/oscars_director.png new file mode 100644 index 000000000..817e52ebd Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/oscars_director.png differ diff --git a/defaults/overlays/images/ribbon/mixed/razzie.png b/defaults/overlays/images/ribbon/mixed/razzie.png new file mode 100644 index 000000000..d61fcc6ee Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/razzie.png differ diff --git a/defaults/overlays/images/ribbon/mixed/rotten.png b/defaults/overlays/images/ribbon/mixed/rotten.png new file mode 100644 index 000000000..c08319523 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/rotten.png differ diff --git a/defaults/overlays/images/ribbon/mixed/rottenverified.png b/defaults/overlays/images/ribbon/mixed/rottenverified.png new file mode 100644 index 000000000..d9e527fb4 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/rottenverified.png differ diff --git a/defaults/overlays/images/ribbon/mixed/spirit.png b/defaults/overlays/images/ribbon/mixed/spirit.png new file mode 100644 index 000000000..ab9520e4f Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/spirit.png differ diff --git a/defaults/overlays/images/ribbon/mixed/sundance.png b/defaults/overlays/images/ribbon/mixed/sundance.png new file mode 100644 index 000000000..32b7e37bb Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/sundance.png differ diff --git a/defaults/overlays/images/ribbon/mixed/venice.png b/defaults/overlays/images/ribbon/mixed/venice.png new file mode 100644 index 000000000..c7692a731 Binary files /dev/null and b/defaults/overlays/images/ribbon/mixed/venice.png differ diff --git a/defaults/overlays/images/ribbon/ribbon-mixed.psd b/defaults/overlays/images/ribbon/ribbon-mixed.psd new file mode 100644 index 000000000..fc560c701 Binary files /dev/null and b/defaults/overlays/images/ribbon/ribbon-mixed.psd differ diff --git a/defaults/overlays/images/streaming/color/Max.png b/defaults/overlays/images/streaming/color/Max.png index 1ab2220c4..fcb9798e8 100644 Binary files a/defaults/overlays/images/streaming/color/Max.png and b/defaults/overlays/images/streaming/color/Max.png differ diff --git a/defaults/overlays/images/streaming/color/NOW.png b/defaults/overlays/images/streaming/color/NOW.png index b58a0d2c7..7024decad 100644 Binary files a/defaults/overlays/images/streaming/color/NOW.png and b/defaults/overlays/images/streaming/color/NOW.png differ diff --git a/defaults/overlays/images/streaming/color/Paramount+.png b/defaults/overlays/images/streaming/color/Paramount+.png index a0156858e..b2d22572c 100644 Binary files a/defaults/overlays/images/streaming/color/Paramount+.png and b/defaults/overlays/images/streaming/color/Paramount+.png differ diff --git a/defaults/overlays/images/streaming/color/tubi.png b/defaults/overlays/images/streaming/color/tubi.png new file mode 100644 index 000000000..4b4ec6066 Binary files /dev/null and b/defaults/overlays/images/streaming/color/tubi.png differ diff --git a/defaults/overlays/images/streaming/white/Disney+.png b/defaults/overlays/images/streaming/white/Disney+.png new file mode 100644 index 000000000..9e8796459 Binary files /dev/null and b/defaults/overlays/images/streaming/white/Disney+.png differ diff --git a/defaults/overlays/images/streaming/white/tubi.png b/defaults/overlays/images/streaming/white/tubi.png new file mode 100644 index 000000000..5f179b24c Binary files /dev/null and b/defaults/overlays/images/streaming/white/tubi.png differ diff --git a/defaults/overlays/images/studio/bigger/Max.png b/defaults/overlays/images/studio/bigger/Max.png new file mode 100644 index 000000000..4f0f2a5d7 Binary files /dev/null and b/defaults/overlays/images/studio/bigger/Max.png differ diff --git a/defaults/overlays/images/studio/standard/Max.png b/defaults/overlays/images/studio/standard/Max.png new file mode 100644 index 000000000..1ab2220c4 Binary files /dev/null and b/defaults/overlays/images/studio/standard/Max.png differ diff --git a/defaults/overlays/languages.yml b/defaults/overlays/languages.yml index 576d6cf02..2d684bb27 100644 --- a/defaults/overlays/languages.yml +++ b/defaults/overlays/languages.yml @@ -50,18 +50,18 @@ external_templates: size: big value: 290 - size: big - value: 250 + value: 216 - three_characters: true value: 230 height: conditions: - size: big - value: 100 + value: 60 final_font_size: default: 50 conditions: - size: big - value: 100 + value: 70 default: style: round country: <> @@ -113,7 +113,7 @@ queues: conditions: - group_alignment: vertical size: big - value: 101 + value: 61 - group_alignment: vertical value: 61 horizontal_spacing: @@ -121,7 +121,7 @@ queues: conditions: - group_alignment: horizontal size: big - value: 251 + value: 217 - group_alignment: horizontal value: 191 surround: diff --git a/defaults/overlays/network.yml b/defaults/overlays/network.yml index d56fb0fac..1ea260c28 100644 --- a/defaults/overlays/network.yml +++ b/defaults/overlays/network.yml @@ -497,6 +497,10 @@ overlays: variables: { weight: 10} template: [name: standard, {name: networks, search: [HBO, HBO Brasil, HBO Europe, HBO Asia, HBO Latin America, HBO España, HBO Nordic, HBO Canada, HBO Family, HBO Mundi]}] + HBO Max: + variables: {key: HBO Max, weight: 10} + template: [name: standard, {name: networks, search: [Max, HBO Go, HBO Max]}] + HGTV: variables: { weight: 10} template: [name: standard, {name: networks, search: [HGTV, HGTV Canada]}] @@ -629,10 +633,6 @@ overlays: variables: { weight: 10} template: [name: standard, name: networks] - Max: - variables: { weight: 10} - template: [name: standard, {name: networks, search: [Max, HBO Go, HBO Max]}] - MBC: variables: { weight: 10} template: [name: standard, name: networks] @@ -965,6 +965,10 @@ overlays: variables: { weight: 10} template: [name: standard, name: networks] + tubi: + variables: { weight: 10} + template: [name: standard, name: networks] + Turner Classic Movies: variables: { weight: 10} template: [name: standard, name: networks] diff --git a/defaults/overlays/resolution.yml b/defaults/overlays/resolution.yml index 6b01cf080..16db4c9e7 100644 --- a/defaults/overlays/resolution.yml +++ b/defaults/overlays/resolution.yml @@ -515,7 +515,7 @@ overlays: variables: {key: 480p, alt: "", weight: 10, type: resolution} template: [name: resolution, name: standard] DV-HDR-Plus: - variables: {key: "", alt: dvhdr, weight: 9, type: resolution, all: true} + variables: {key: "", alt: dvhdrplus, weight: 9, type: resolution, all: true} template: [name: resolution, name: standard] DV-HDR: variables: {key: "", alt: dvhdr, weight: 8, type: resolution, all: true} @@ -601,4 +601,4 @@ overlays: template: [name: edition, name: standard] Ulysses: variables: {key: ulysses, weight: 5, search: Ulysses, type: edition, allowed_libraries: movie} - template: [name: edition, name: standard] + template: [name: edition, name: standard] \ No newline at end of file diff --git a/defaults/overlays/ribbon.yml b/defaults/overlays/ribbon.yml index dbf472909..64b1f749f 100644 --- a/defaults/overlays/ribbon.yml +++ b/defaults/overlays/ribbon.yml @@ -20,6 +20,8 @@ external_templates: value: black - style: red value: red + - style: mixed + value: mixed default: conditions: - file.exists: false diff --git a/defaults/overlays/streaming.yml b/defaults/overlays/streaming.yml index 2c88579e0..86bd223b1 100644 --- a/defaults/overlays/streaming.yml +++ b/defaults/overlays/streaming.yml @@ -132,8 +132,8 @@ overlays: variables: {key: disney, tmdb_key: 337|508, weight: 140} template: [name: standard, name: mdb_streaming] - Max: - variables: {key: max, tmdb_key: 1899, weight: 130} + HBO-Max: + variables: {key: hbomax, tmdb_key: 1899, weight: 130} template: [name: standard, name: mdb_streaming] Crunchyroll: @@ -187,3 +187,7 @@ overlays: hayu: variables: {key: hayu, tmdb_key: 223, weight: 10, allowed_libraries: show} template: [name: standard, name: mdb_streaming] + + tubi: + variables: {key: hayu, tmdb_key: 73, weight: 5} + template: [name: standard, name: mdb_streaming] diff --git a/defaults/overlays/studio.yml b/defaults/overlays/studio.yml index 2aa89f13f..7b2fceeba 100644 --- a/defaults/overlays/studio.yml +++ b/defaults/overlays/studio.yml @@ -665,7 +665,7 @@ overlays: template: [name: standard, name: studios] Amazon Studios: - template: [name: standard, {name: studios, search: [Amazon, Amazon Studios]}] + template: [name: standard, {name: studios, search: [Amazon, Amazon Studios, Prime Video]}] Amblin Entertainment: template: [name: standard, {name: studios, search: [Amblin Entertainment, Amblin Television]}] @@ -1123,6 +1123,9 @@ overlays: Matt Tolmach Productions: template: [name: standard, name: studios] + Max: + template: [name: standard, {name: studios, search: [HBO Max]}] + Maximum Effort: template: [name: standard, name: studios] @@ -1481,13 +1484,13 @@ overlays: template: [name: standard, name: studios] Walt Disney Pictures: - template: [name: standard, {name: studios, search: [Disney, Walt Disney Pictures]}] + template: [name: standard, {name: studios, search: [Disney+, Disney, Walt Disney Pictures]}] Walt Disney Productions: template: [name: standard, name: studios] Warner Animation Group: - template: [name: standard, {name: studios, search: [Warner Animation Group, Warner Bros. Cartoon Studios, Warner Animation]}] + template: [name: standard, {name: studios, search: [Warner Animation Group, Warner Bros. Cartoon Studios, Warner Bros. Cartoons, Warner Animation]}] Warner Bros. Pictures: template: [name: standard, {name: studios, search: [Warner, Warner Bros. Pictures]}] @@ -1523,4 +1526,4 @@ overlays: template: [name: standard, name: studios] Working Title Films: - template: [name: standard, name: studios] \ No newline at end of file + template: [name: standard, name: studios] diff --git a/defaults/overlays/versions.yml b/defaults/overlays/versions.yml index 848d131a2..e4944fa93 100644 --- a/defaults/overlays/versions.yml +++ b/defaults/overlays/versions.yml @@ -10,24 +10,27 @@ external_templates: default: templates template_variables: default: - vertical_align: top + horizontal_align: right + vertical_align: bottom conditionals: horizontal_align: default: right conditions: - builder_level: episode - value: center + value: right - builder_level: episode - value: center + value: right vertical_offset: default: 15 conditions: - vertical_align.exists: false builder_level.not: episode - value: 1060 + value: 335 - vertical_align.exists: false builder_level.not: episode - value: 1060 + value: 335 + - vertical_align.exists: false + value: 270 - vertical_align: center value: 0 - vertical_align: top @@ -77,4 +80,4 @@ templates: overlays: versions: - template: [name: standard, name: versions] \ No newline at end of file + template: [name: standard, name: versions] diff --git a/defaults/show/franchise.yml b/defaults/show/franchise.yml index fc7cf4c2e..f2130e7d7 100644 --- a/defaults/show/franchise.yml +++ b/defaults/show/franchise.yml @@ -81,18 +81,24 @@ dynamic_collections: "79744": The Rookie "80748": FBI "85536": Star Wars + "46296": Spartacus + "108978": Reacher + "60585": Bosch + "3476": Inspector Morse + "61511": Father Brown (2013) + "41956": Death in Paradise (2011) template: - tmdbshow addons: 121: [57243, 1057, 424, 203, 64073] # Doctor Who: K-9 & Company, Torchwood, The Sarah Jane Adventures, Class 253: [655, 580, 1855, 1992, 314, 103516, 67198, 82491, 85948, 85949, 106393] # TOS, TAS, TNG, DS9, VOY, ENT, DIS, SHO, PIC, LOW, PRO, SNW - 549: [2734, 4601, 3357, 32632, 72496, 106158] # Law & Order: Special Victims Unit, Criminal Intent, Trial by Jury, LA, True Crime, Organized Crime + 549: [2734, 4601, 3357, 32632, 72496, 106158, 7098] # Law & Order: Special Victims Unit, Criminal Intent, Trial by Jury, LA, True Crime, Organized Crime, UK 951: [25641, 4489, 24211, 9829, 605, 69050, 79242, 87539] # The Archie Show, Sabrina, The Teenage Witch, Josie and the Pussycats, Josie and the Pussycats in Outer Space, The New Archies, Riverdale, Chilling Adventures of Sabrina, Katy Keene 1399: [94997] # Game of Thrones, House of the Dragon 1402: [62286, 94305, 194583, 211684, 206586] # The Walking Dead: Fear the Walking Dead, World Beyond, Dead City, Daryl Dixon, The Ones Who Live 1412: [60735, 62688, 62643, 71663, 89247] # Arrow: The Flash, Supergirl, Legends of Tomorrow, Black Lightning, Batwoman 1431: [1620, 2458, 122194, 61811] # CSI: Miami, NY, Vegas, Cyber - 4614: [17610, 124271, 61387, 4376, 157950] # NCIS: Los Angeles, Hawaii, New Orleans, JAG, Sydney + 4614: [17610, 124271, 61387, 4376, 157950, 243006, 247732] # NCIS: Los Angeles, Hawaii, New Orleans, JAG, Sydney, Origins, Tony & Ziva 4629: [2290, 5148, 72925] # Stargate SG-1: Atlantis, Universe, Origins 8514: [200870, 212798, 38409, 200865, 67482, 124045, 216276, 77233, 92611, 98888, 94038, 127841, 67564, 122692, 139203, 203734, 108934, 204701, 210689, 155431, 106475, 152261, 66769] #Drag Race: Sweden, Sweden Untucked, Untucked, Belgium, All-Stars, Spain, Brazil, Thailand, UK, Secret Celebrity, Canada, Italy, All Stars Untucked, Australia, Philippines, Canada VS the World, Holland, Philippines Untucked, Spain Untucked, UK VS the World, Vegas Revue, France, Ruvealed 6357: [1918, 83135, 16399] # The Twilight Zone (multiple) @@ -104,4 +110,10 @@ dynamic_collections: 75219: 89393 # 9-1-1: 9-1-1 Lone Star 79744: 201992 # The Rookie: Feds 80748: [94372, 121658] # FBI: Most Wanted, International - 85536: [71412, 3478, 105971, 92830, 83867, 60554, 82856, 115036, 114461, 202879, 114478, 79093] # Star Wars Galaxy of Adventures: Forces of Destiny, The Clone Wars, The Bad Batch, Obi-Wan Kenobi, Andor, Rebels, The Mandalorian, The Book of Boba Fett, Ahsoka, Skeleton Crew, Visions, Resistance + 85536: [71412, 3478, 105971, 92830, 83867, 60554, 82856, 115036, 114461, 202879, 114478, 79093, 4194, 203085] # Star Wars Galaxy of Adventures: Forces of Destiny, Ewoks, The Bad Batch, Obi-Wan Kenobi, Andor, Rebels, The Mandalorian, The Book of Boba Fett, Ahsoka, Skeleton Crew, Visions, Resistance, Clone Wars, Tales of the Jedi + 46296: 240459 # Spartacus: House of Ashur + 108978: 273207 # Neagley + 60585: 153657 # Bosch: Legacy + 3476: [44264, 2343] # Inspector Morse, Endeavour, Lewis + 61511: 157282 # Sister Boniface Mysteries (2022) + 41956: [205072, 244978] # Beyond Paradise (2023), Return to Paradise (2025) diff --git a/defaults/show/network.yml b/defaults/show/network.yml index 16594670e..f2b642a92 100644 --- a/defaults/show/network.yml +++ b/defaults/show/network.yml @@ -27,7 +27,7 @@ dynamic_collections: - shared template_variables: delete_collections_named: - Max: HBO Max + HBO Max: Max search_term: default: network image: @@ -181,7 +181,6 @@ dynamic_collections: - Logo - Magnolia Network - MasterClass - - Max - MBC - MBN - MGM+ @@ -265,6 +264,7 @@ dynamic_collections: - Tokyo MX - Travel Channel - truTV + - tubi - Turner Classic Movies - TV 2 - tv asahi @@ -413,6 +413,9 @@ dynamic_collections: - HBO Canada - HBO Family - HBO Mundi + HBO Max: + - HBO Go + - Max HGTV: - HGTV Canada History: @@ -420,9 +423,6 @@ dynamic_collections: - H2 Lifetime: - Lifetime Movies - Max: - - HBO Go - - HBO Max MTV: - MTV2 - MTV3 diff --git a/docker_run.sh b/docker_run.sh new file mode 100644 index 000000000..7751a35af --- /dev/null +++ b/docker_run.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pip install -r /kometa/requirements.txt +python /kometa/kometa.py -run +sleep infinity \ No newline at end of file diff --git a/docker_run_v2.sh b/docker_run_v2.sh new file mode 100644 index 000000000..9fb2228aa --- /dev/null +++ b/docker_run_v2.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Install Kometa requirements +pip install -r /kometa/requirements.txt + +# Define the base path +BASE_PATH="/kometa" + +# Define the libraries +MOVIE_LIBRARY="Filme" +MOVIE_UHD_LIBRARY="Filme UHD" +MOVIE_SERIALS_LIBRARY="Movie Serials" +MUSIC_LIBRARY="Musik - Live" +SERIES_LIBRARY="Serien" + + +LIBRARIES=("$MOVIE_LIBRARY" "$MOVIE_UHD_LIBRARY" "$MOVIE_SERIALS_LIBRARY" "$MUSIC_LIBRARY" "$SERIES_LIBRARY" "") + +# Function to run Kometa for all libraries +run_all() { + python $BASE_PATH/kometa.py -r +} + +# Function to update posters for a specific library +update_posters() { + local library="$1" + python $BASE_PATH/kometa.py -r -ov -rl "$library" +} + +# Function to update collections for a specific library +update_collection() { + local library="$1" + python $BASE_PATH/kometa.py -r -co -rl "$library" +} + +# Function to update collections for a specific library +update_metadata() { + local library="$1" + python $BASE_PATH/kometa.py -r -mo -rl "$library" +} + +# Function to update collections for a specific library +do_operations() { + local library="$1" + python $BASE_PATH/kometa.py -r -op -rl "$library" +} + +# Function for a complete update of a library +run_complete() { + local library="$1" + python $BASE_PATH/kometa.py -r -rl "$library" +} + +# Select library from a list +select_library() { + select lib in "${LIBRARIES[@]}"; do + if [[ -n "$lib" ]]; then + echo "$lib" + return + else + echo "Invalid selection. Try again." + fi + done +} + +# Main menu +while true; do + clear + echo "Kometa Docker Menu:" + echo " 1) Run All (kometa -r)" + echo " 2) Filme - Complete Update" + echo " 3) Filme UHD - Complete Update" + echo " 4) Movie Serials - Complete Update" + echo " 5) Musik - Live - Complete Update" + echo " 6) Serien - Complete Update" + echo " 7) Update Posters for a Library" + echo " 8) Update Collection for a Library" + echo " 9) Update Metadata for a Library" + echo "10) Do operations for a Library" + echo "11) Exit" + + read -p "Enter your choice: " choice + + case "$choice" in + 1) run_all ;; + 2) run_complete "$MOVIE_LIBRARY" ;; + 3) run_complete "$MOVIE_UHD_LIBRARY" ;; + 4) run_complete "$MOVIE_SERIALS_LIBRARY" ;; + 5) run_complete "$MUSIC_LIBRARY" ;; + 6) run_complete "$SERIES_LIBRARY" ;; + 7) + lib=$(select_library) + [ -n "$lib" ] && update_posters "$lib" + ;; + 8) + lib=$(select_library) + [ -n "$lib" ] && update_collection "$lib" + ;; + 9) + lib=$(select_library) + [ -n "$lib" ] && update_metadata "$lib" + ;; + 10) + lib=$(select_library) + [ -n "$lib" ] && do_operations "$lib" + ;; + 11) exit ;; + *) echo "Invalid option. Try again." ;; + esac + + read -p "Press Enter to continue..." +done diff --git a/docs/assets/css/extra.css b/docs/assets/css/extra.css index 4671318bd..5d410312e 100644 --- a/docs/assets/css/extra.css +++ b/docs/assets/css/extra.css @@ -771,12 +771,10 @@ code { .md-typeset pre { position: relative; - overflow: hidden; } .md-typeset pre>code { display: block; - overflow-x: auto; padding-right: 3rem; white-space: pre; } @@ -862,3 +860,18 @@ code { .md-typeset pre>code { scrollbar-color: #03a37a #0000; } + +div[id*="tippy"] h1, +div[id*="tippy"] h2, +div[id*="tippy"] h3, +div[id*="tippy"] h4, +div[id*="tippy"] h5, +div[id*="tippy"] h6 { + margin-top: 0.3rem; + margin-bottom: 0.3rem; +} + +[data-tooltip][data-tooltip-id*="tippy"] { + color: rgb(4 181 181); +} + diff --git a/docs/assets/images/config/github-classic.png b/docs/assets/images/config/github-classic.png new file mode 100644 index 000000000..d7285eda6 Binary files /dev/null and b/docs/assets/images/config/github-classic.png differ diff --git a/docs/assets/images/config/github-finegrain.png b/docs/assets/images/config/github-finegrain.png new file mode 100644 index 000000000..6dd9dcca6 Binary files /dev/null and b/docs/assets/images/config/github-finegrain.png differ diff --git a/docs/assets/images/defaults/overlays/language2.png b/docs/assets/images/defaults/overlays/languages2.png similarity index 100% rename from docs/assets/images/defaults/overlays/language2.png rename to docs/assets/images/defaults/overlays/languages2.png diff --git a/docs/assets/images/defaults/overlays/language3.png b/docs/assets/images/defaults/overlays/languages3.png similarity index 100% rename from docs/assets/images/defaults/overlays/language3.png rename to docs/assets/images/defaults/overlays/languages3.png diff --git a/docs/assets/images/defaults/overlays/version.png b/docs/assets/images/defaults/overlays/versions.png similarity index 100% rename from docs/assets/images/defaults/overlays/version.png rename to docs/assets/images/defaults/overlays/versions.png diff --git a/docs/assets/images/kometa/install/truenas/Kometa Installation Walkthrough for TrueNAS Scale.pdf b/docs/assets/images/kometa/install/truenas/Kometa Installation Walkthrough for TrueNAS Scale.pdf new file mode 100644 index 000000000..06c44ae02 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/Kometa Installation Walkthrough for TrueNAS Scale.pdf differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-1.png b/docs/assets/images/kometa/install/truenas/truenas-step-1.png new file mode 100755 index 000000000..4b519017e Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-1.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-1.pxd b/docs/assets/images/kometa/install/truenas/truenas-step-1.pxd new file mode 100644 index 000000000..06b3de353 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-1.pxd differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-2a.png b/docs/assets/images/kometa/install/truenas/truenas-step-2a.png new file mode 100644 index 000000000..3511bab31 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-2a.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-2a.pxd b/docs/assets/images/kometa/install/truenas/truenas-step-2a.pxd new file mode 100644 index 000000000..5a0e559b3 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-2a.pxd differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-2b.png b/docs/assets/images/kometa/install/truenas/truenas-step-2b.png new file mode 100755 index 000000000..94ca2410c Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-2b.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-2b.pxd b/docs/assets/images/kometa/install/truenas/truenas-step-2b.pxd new file mode 100644 index 000000000..5e80e0384 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-2b.pxd differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-2c.png b/docs/assets/images/kometa/install/truenas/truenas-step-2c.png new file mode 100755 index 000000000..7d2c7413a Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-2c.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-3.png b/docs/assets/images/kometa/install/truenas/truenas-step-3.png new file mode 100755 index 000000000..eb9843335 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-3.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-4a.png b/docs/assets/images/kometa/install/truenas/truenas-step-4a.png new file mode 100755 index 000000000..c8ce87fa6 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-4a.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-4b.png b/docs/assets/images/kometa/install/truenas/truenas-step-4b.png new file mode 100644 index 000000000..01c5a6894 Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-4b.png differ diff --git a/docs/assets/images/kometa/install/truenas/truenas-step-4b.pxd b/docs/assets/images/kometa/install/truenas/truenas-step-4b.pxd new file mode 100644 index 000000000..51f7f090d Binary files /dev/null and b/docs/assets/images/kometa/install/truenas/truenas-step-4b.pxd differ diff --git a/docs/assets/images/kometa/install/unraid-console.png b/docs/assets/images/kometa/install/unraid-console.png new file mode 100644 index 000000000..970205bfe Binary files /dev/null and b/docs/assets/images/kometa/install/unraid-console.png differ diff --git a/docs/assets/js/extra.js b/docs/assets/js/extra.js index f45693fc3..686f1b77a 100644 --- a/docs/assets/js/extra.js +++ b/docs/assets/js/extra.js @@ -83,3 +83,15 @@ document$.subscribe(function() { !function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a,b){return a.getAttribute(b.sortAttribute||"data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&n.push(m),o++;if(!n)return}for(o=0;o*Required only when trying to use multiple servers with the same name.* - Each library that the user wants Kometa to interact with must be documented with a library attribute. + Each library that the user wants Kometa to interact with must be documented with a library attribute. - A library attribute is represented by the mapping name (i.e. `Movies` or `TV Shows`), this must have a unique name that correlates with a + A library attribute is represented by the mapping name (i.e. `Movies` or `TV Shows`), this must have a unique name that correlates with a library of the same name within the Plex Media Server. - - In the situation that two servers are being connected to which both have libraries of the same name, the `library_name` attribute can be utilized to specify the real + + In the situation that two servers are being connected to which both have libraries of the same name, the `library_name` attribute can be utilized to specify the real Library Name, whilst the library attribute's mapping name can be made into a placeholder. This is showcased below:
- + **Attribute:** `library_name` - + **Accepted Values:** Library Name. **Default Value:** Base Attribute Name ???+ example "Example" - + ```yaml libraries: Movies01: @@ -53,25 +53,69 @@ The available attributes for each library are as follows: url: http://192.168.1.12:32400 token: #################### ``` - - * In this example, `"Movies01"`, `"TV Shows"`, and `"Anime"` will all use the global plex server (**http://192.168.1.12:32400**) which is defined using the global + + * In this example, `"Movies01"`, `"TV Shows"`, and `"Anime"` will all use the global plex server (**http://192.168.1.12:32400**) which is defined using the global `plex` mapping. `"Movies02"` will use the plex server **http://192.168.1.35:32400** which is defined under its `plex` mapping over the global mapping. +??? blank "`language` - Used to define the language for generated collection titles and metadata." + +
The `language` attribute sets the language used by Kometa for **default collection names, separators, and other translated labels** during config generation. + + This setting is useful when creating libraries for non-English speakers or when using pre-translated default YAML files. It only affects **metadata generated by Kometa**—it does not modify Plex language preferences. + +
+ + **Attribute:** `language` + + **Accepted Values:** One of the supported language codes below. + + **Default Value:** `en` + + | Code | Language | + |---------|---------------------| + | `ar` | Arabic | + | `da` | Danish | + | `de` | German | + | `en` | English | + | `es` | Spanish | + | `fr` | French | + | `it` | Italian | + | `nb_NO` | Norwegian (Norway) | + | `nl` | Dutch | + | `pt-br` | Portuguese (Brazil) | + | `ru` | Russian | + | `sv` | Swedish | + + To contribute or improve translations, visit the [Kometa Translations Weblate Site](https://translations.kometa.wiki/). + + ???+ example "Example" + + ```yaml + libraries: + FrenchLibrary: + library_name: Films + template_variables: + language: fr + sep_style: gray + ``` + + In this example, the library `"FrenchLibrary"` will use the French (`fr`) language for all default collection names and related metadata Kometa generates. + ??? blank "`collection_files` - Used to define [Collection Files](../files/collections.md)." -
The `collection_files` attribute is used to define [Collection Files](../files/collections.md) by specifying +
The `collection_files` attribute is used to define [Collection Files](../files/collections.md) by specifying the path type and path of the files that will be executed against the parent library. See [File Blocks](files.md) for how to define them.
- + **Attribute:** `collection_files` - + **Accepted Values:** Location of Collection YAML files. **Default Value:** `/config/<>.yml` ???+ example "Example" - + ```yaml libraries: TV Shows: @@ -81,9 +125,9 @@ The available attributes for each library are as follows: - default: network ``` - By default, when `collection_files` is missing Kometa will look within the root Kometa directory for a Collection File called + By default, when `collection_files` is missing Kometa will look within the root Kometa directory for a Collection File called `.yml`. In the example below, Kometa will look for a file named `TV Shows.yml`. - + ```yaml libraries: TV Shows: @@ -96,22 +140,22 @@ The available attributes for each library are as follows: the files that will be executed against the parent library. See [File Blocks](files.md) for how to define them. ???+ tip - - As of Kometa 1.20.0 "Metadata Files" refers to YAML files which refers to managing the metadata of items [movies, shows, music] + + As of Kometa 1.20.0 "Metadata Files" refers to YAML files which refers to managing the metadata of items [movies, shows, music] within your library, and "Collection Files" refers to YAML files which define Collections. - + In previous version of Kometa, "Metadata Files" could mean either of the above.
- + **Attribute:** `metadata_files` - + **Accepted Values:** Location of [Metadata Files](../files/metadata.md). **Default Value:** `None` ???+ example "Example" - + ```yaml libraries: TV Shows: @@ -121,19 +165,19 @@ The available attributes for each library are as follows: ??? blank "`overlay_files` - Used to define [Overlay Files](../files/overlays.md)." -
The `overlay_files` attribute is used to define [Overlay Files](../files/overlays.md) by specifying the path +
The `overlay_files` attribute is used to define [Overlay Files](../files/overlays.md) by specifying the path type and path of the files that will be executed against the parent library. See [File Blocks](files.md) for how to define them.
- + **Attribute:** `overlay_files` - + **Accepted Values:** Location of [Overlay Files](../files/overlays.md). **Default Value:** `None` ???+ example "Example" - + ```yaml libraries: TV Shows: @@ -145,22 +189,22 @@ The available attributes for each library are as follows: ??? blank "`report_path` - Location to save the YAML Report file for a library." -
The `report_path` attribute is used to define where to save the YAML Report file. This file is used to store information about what media is added, +
The `report_path` attribute is used to define where to save the YAML Report file. This file is used to store information about what media is added, removed, filtered, and missing from the Plex library compared to what is expected from the Collection, Metadata, Overlay or Playlist file. - - If your Collection File creates a collection with `Movie 1`, `Movie 2` and `Movie 3` but your Plex library only has `Movie 1` and `Movie 3`, + + If your Collection File creates a collection with `Movie 1`, `Movie 2` and `Movie 3` but your Plex library only has `Movie 1` and `Movie 3`, then the missing YAML file will be updated to inform the user that `Movie 2` was missing from the library. - +
- + **Attribute:** `report_path` - + **Accepted Values:** Location to save the YAML Report file. **Default Value:** `/config/<>_report.yml` (Where `<>` is the name of the library attribute.) ???+ example "Example" - + If you want to call your Report YAML something different you can like so: ```yaml @@ -168,9 +212,9 @@ The available attributes for each library are as follows: Movies: report_path: /config/My Movie Report.yml ``` - + Alternatively, Report YAML files can be placed in their own directory, as below: - + ```yaml libraries: Movies: @@ -183,19 +227,19 @@ The available attributes for each library are as follows: ??? blank "`template_variables` - Used to define [Custom Template Variables](../files/templates.md#template-variables) for every file in a library." -
Passes all given [Template Variables](../files/templates.md#template-variables) +
Passes all given [Template Variables](../files/templates.md#template-variables) to every template in every Collection, Metadata, and Overlay File run. - +
- + **Attribute:** `template_variables` - - **Accepted Values:** [Dictionary](../kometa/yaml.md#dictionaries) of values specified by each particular file. + + **Accepted Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of values specified by each particular file. **Default Value:** `None` ???+ example "Example" - + ```yaml libraries: Movies: @@ -210,17 +254,17 @@ The available attributes for each library are as follows: ??? blank "`schedule` - Used to schedule when a library is run."
Used to schedule when a library is run using the [schedule options](schedule.md). - +
- + **Attribute:** `schedule` - + **Accepted Values:** Any [schedule option](schedule.md). **Default Value:** `daily` ???+ example "Example" - + ```yaml libraries: TV Shows: @@ -234,17 +278,17 @@ The available attributes for each library are as follows: ??? blank "`operations` - Used to specify [Library Operations](operations.md) to run."
Used to specify [Library Operations](operations.md) to run. - +
- + **Attribute:** `operations` - + **Accepted Values:** Any [Library Operation](operations.md). **Default Value:** `None` ???+ example "Example" - + ```yaml libraries: Movies: @@ -257,28 +301,28 @@ The available attributes for each library are as follows: ??? blank "`remove_overlays` - Used to remove overlays." -
Used to remove overlays from this library only. +
Used to remove overlays from this library only. - Kometa will aim to use the Original Posters backup that it created in the "overlays" folder to restore from, and will be unable to remove the overlays if this backup no longer exists. + Kometa will aim to use the Original Posters backup that it created in the "overlays" folder to restore from, and will be unable to remove the overlays if this backup no longer exists. Kometa will also remove the `Overlay` label from the items in Plex. The result of setting `remove_overlays` is your Plex library should no longer have any Overlays applied by Kometa. ???+ warning "Proceed with Caution" - When set to `true`, this will remove all overlays from your library every run, but will not delete + When set to `true`, this will remove all overlays from your library every run, but will not delete the overlaid images from your system, resulting in [image bloat](../kometa/scripts/imagemaid.md).
- + **Attribute:** `remove_overlays` - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml libraries: Movies: @@ -304,15 +348,15 @@ The available attributes for each library are as follows: In general use, this setting will only extend runtimes and cause image bloat in the Plex metadata for no good reason.
- + **Attribute:** `reapply_overlays` - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml libraries: Movies: @@ -327,27 +371,27 @@ The available attributes for each library are as follows:
Used to reset the base image used for overlays from this library only. - Kometa will fetch a new "base" image from the desired source, and will use that as the new Original Poster upon which to apply overlays as part of the run. + Kometa will fetch a new "base" image from the desired source, and will use that as the new Original Poster upon which to apply overlays as part of the run. The result of setting `reset_overlays` is that your Plex library will have Overlays applied based upon the new images taken from the source specified. - + ???+ warning "Proceed with Caution" This will reset all posters to the desired source on each run and will reapply all overlays on each run, which will result in [image bloat](../kometa/scripts/imagemaid.md). - Additionally, any image obtained from this setting will take priority over any image you set using an Asset Directory. If you use Asset Directories, + Additionally, any image obtained from this setting will take priority over any image you set using an Asset Directory. If you use Asset Directories, you shouldn't really be using this setting as the Asset Directory should be the single source of truth for what the "base" image is.
- + **Attribute:** `reset_overlays` - + **Accepted Values:** `plex` or `tmdb`. **Default Value:** `None` ???+ example "Example" - + ```yaml libraries: Movies: @@ -364,15 +408,15 @@ The available attributes for each library are as follows: cannot schedule individual Overlay Files, as any unscheduled Overlay File will be removed each time Kometa is run.
- + **Attribute:** `schedule_overlays` - + **Accepted Values:** [Any Schedule Option](schedule.md). **Default Value:** `daily` ???+ example "Example" - + ```yaml libraries: TV Shows: @@ -386,17 +430,17 @@ The available attributes for each library are as follows: ??? blank "`settings` - Used to override global [`setting` attributes](settings.md) for this library only."
Used to override global [`setting` attributes](settings.md) for this library only. - +
- + **Attribute:** `settings` - + **Accepted Values:** Any [`setting`](settings.md) attribute that overrides a global value. **Default Value:** Global Value ???+ example "Example" - + ```yaml libraries: Movies: @@ -408,20 +452,20 @@ The available attributes for each library are as follows: ??? blank "`plex` - Used to override global [`plex` attributes](plex.md) for this library only." -
Used to override global [`plex` attributes](plex.md) for this library only. +
Used to override global [`plex` attributes](plex.md) for this library only. **`plex` Attribute is required either here or globally** - +
- + **Attribute:** `plex` - + **Accepted Values:** Any [`plex`](plex.md) attribute that overrides a global value. **Default Value:** Global Value ???+ example "Example" - + ```yaml libraries: Movies: @@ -451,36 +495,36 @@ The available attributes for each library are as follows: ??? blank "`radarr` - Used to override global [`radarr` attributes](radarr.md) for this library only."
Used to override global [`radarr` attributes](radarr.md) for this library only. - +
- + **Attribute:** `radarr` - + **Accepted Values:** Any [`radarr`](radarr.md) attribute that overrides a global value. **Default Value:** Global Value ???+ example "Example" - + ```yaml libraries: Library01: # this library uses the default radarr config collection_files: - file: config/Movies.yml - + Library02: # this library overrides radarr root path and profile collection_files: - file: config/Movies.yml radarr: root_folder_path: /data/media/movies/tony quality_profile: Better - + Library03: # this library overrides radarr quality profile collection_files: - file: config/Movies.yml radarr: quality_profile: Best - + Library04: # this library uses the 4K radarr instance collection_files: - file: config/Movies.yml @@ -489,7 +533,7 @@ The available attributes for each library are as follows: token: SOME_OTHER_TOKEN root_folder_path: /data/media/movies/geezer quality_profile: Bestest - + Library05: # movies get added by a custom script so they should get added to radarr-4k collection_files: - file: config/Movies.yml @@ -499,7 +543,7 @@ The available attributes for each library are as follows: root_folder_path: /data/media/movies/bill quality_profile: Bestest add_existing: true - sonarr_path: /data/media/movies/bill + radarr_path: /data/media/movies/bill plex_path: /mnt/unionfs/movies/bill ... radarr: @@ -523,36 +567,36 @@ The available attributes for each library are as follows: ??? blank "`sonarr` - Used to override global [`sonarr` attributes](sonarr.md) for this library only."
Used to override global [`sonarr` attributes](sonarr.md) for this library only. - +
- + **Attribute:** `sonarr` - + **Accepted Values:** Any [`sonarr`](sonarr.md) attribute that overrides a global value. **Default Value:** Global Value ???+ example "Example" - + ```yaml libraries: Library01: # this library uses the default sonarr config collection_files: - file: config/TV.yml - + Library02: # this library overrides sonarr root path and profile collection_files: - file: config/TV.yml sonarr: root_folder_path: /data/media/shows/tony quality_profile: Better - + Library03: # this library overrides sonarr quality profile collection_files: - file: config/TV.yml sonarr: quality_profile: Best - + Library04: # this library uses the 4K sonarr instance collection_files: - file: config/TV.yml @@ -561,7 +605,7 @@ The available attributes for each library are as follows: token: SOME_OTHER_TOKEN root_folder_path: /data/media/shows/geezer quality_profile: Bestest - + Library05: # shows get added by a custom script so they should get added to sonarr-4k collection_files: - file: config/TV.yml @@ -573,7 +617,7 @@ The available attributes for each library are as follows: add_existing: true sonarr_path: /data/media/shows/bill plex_path: /mnt/unionfs/shows/bill - + ... sonarr: url: https://sonarr.bing.bang @@ -599,17 +643,17 @@ The available attributes for each library are as follows: ??? blank "`tautulli` - Used to override global [`tautulli` attributes](tautulli.md) for this library only."
Used to override global [`tautulli` attributes](tautulli.md) for this library only. - +
- + **Attribute:** `tautulli` - + **Accepted Values:** Any [`tautulli`](tautulli.md) attribute that overrides a global value. **Default Value:** Global Value ???+ example "Example" - + ```yaml libraries: Movies: @@ -635,7 +679,7 @@ This example is an advanced version of the library mappings which highlights som ???+ example "Example Library Mappings" In this example, the `"TV Shows On Second Plex"` library has a library-level `plex` configuration, which takes priority over the `plex` configuration set at the global level. - + The `"Anime"` library also has a library-level `radarr` configuration, which takes priority over the `radarr` configuration set at the global level. ```yaml diff --git a/docs/config/myanimelist.md b/docs/config/myanimelist.md index fcce065fd..a9d2385fa 100644 --- a/docs/config/myanimelist.md +++ b/docs/config/myanimelist.md @@ -27,7 +27,11 @@ mal: | `localhost_url` | Redirect URL used for authorization flow. | Valid localhost URL or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | -*All other attributes will be filled in by Kometa as part of the authentication process** +*All other attributes will be filled in by Kometa as part of the authentication process* + +### Important Note on "Authentication Process": + +The MyAnimeList authentication process is interactive; Kometa will display a URL in the console output and then wait for you to visit that URL in order to grant access and then paste in some additional information. In order for this to happen you need to run Kometa in an interactive mode, which can be fussy in some contexts (e.g., running Kometa in a Docker container on a NAS). For this reason, it is far simpler to use the form down below to perform these steps; it does all the same steps, but takes them out of the Kometa script execution. The form will produce a complete authentication block as shown above ready for you to copy-paste into your `config.yml`. ## MyAnimeList Authentication diff --git a/docs/config/operations.md b/docs/config/operations.md index 5511d5bdb..d98e3309f 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -24,7 +24,7 @@ libraries: ## Operation Blocks -You can create individual blocks of operations by using a list under `operations` with each item in the list being a "block" that can be individually scheduled. +You can create individual blocks of operations by using a list under `operations` with each item in the list being a "block" that can be individually scheduled. ```yaml title="config.yml Operation Blocks sample" libraries: @@ -40,27 +40,33 @@ libraries: ## A Note on Data Sources -Several of these operations use data from external sources such as MDBList. Some of these sources have free and paid tiers of access. -Your entire library may not be able to be updated in a single run on a free tier; you may need to make multiple runs over some number of days. +Several of these operations use data from external sources such as MDBList. You will need to authenticate against each service you wish to use in these operations. + +Some of these sources have free and paid tiers of access. Your entire library may not be able to be updated in a single run on a free tier; you may need to make multiple runs over some number of days. + For example, MDBList's free tier is limited [at time of writing] to 1000 API requests daily. Changes in external site paid/free status and limits are not under Kometa's control. +## A Note on Mass Operations + +Several of these operations perform **mass** updates; these are just that, **mass** updates. Except as shown below, they cannot be filtered to operate on only a subset of the items in your library. + ## Operation Attributes ###### Assets For All ??? blank "`assets_for_all` - Used to search the asset directories for images for all items in the library." -
Ordinarily, Kometa searches the asset directories for collection artwork. Enabling this Operation tells Kometa +
Ordinarily, Kometa searches the asset directories for collection artwork. Enabling this Operation tells Kometa to searches the asset directories for images for all items [movies, shows, seasons, episodes, etc] in the library. - +
- + **Attribute:** `assets_for_all` - + **Accepted Values:** `true` or `false`. ???+ example "Example" - + ```yaml libraries: Movies: @@ -68,18 +74,40 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req assets_for_all: false ``` +###### Assets For All Collections + +??? blank "`assets_for_all_collections` - Used to search the asset directories for images for all unmanaged and/or unconfigured in the library." + +
Enabling this Operation tells Kometa + to search the asset directories for images for unmanaged and unconfigured collections in the library. + +
+ + **Attribute:** `assets_for_all_collections` + + **Accepted Values:** `true` or `false`. + + ???+ example "Example" + + ```yaml + libraries: + Movies: + operations: + assets_for_all_collections: false + ``` + ###### Delete Collections ??? blank "`delete_collections` - Deletes collections based on a set of given attribute."
Deletes collections based on a set of given attributes. The Collection must match all set attributes to be deleted. - +
- + **Attribute:** `delete_collections` - + **Accepted Values:** There are a few different options to determine how the `delete_collections` works. - + @@ -94,7 +122,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ???+ example "Example" Removes all Managed Collections (Collections with the `Kometa` Label) that are not configured in the Current Run. - + ```yaml libraries: Movies: @@ -109,31 +137,31 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_genre_update` - Updates the genres of every item in the library."
Updates every item's genres in the library to the chosen site's genres. - +
- + **Attribute:** `mass_genre_update` - - **Accepted Values:** Source or List of sources to use in that order. + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order.
`managed: true`Collection must be Managed to be deleted.
(collection has the `Kometa` label)
`managed: false`Collection must be Unmanaged to be deleted.
(collection does not have the `Kometa` label)
- - - - - - - - - + + + + + + + + + - +
`tmdb`Use TMDb for Genres.
`tvdb`Use TVDb for Genres.
`imdb`Use IMDb for Genres.
`omdb`Use IMDb through OMDb for Genres.
`anidb`Use AniDB Main Tags for Genres.
`anidb_3_0`Use AniDB Main Tags and All 3 Star Tags and above for Genres.
`anidb_2_5`Use AniDB Main Tags and All 2.5 Star Tags and above for Genres.
`anidb_2_0`Use AniDB Main Tags and All 2 Star Tags and above for Genres.
`anidb_1_5`Use AniDB Main Tags and All 1.5 Star Tags and above for Genres.
`anidb_1_0`Use AniDB Main Tags and All 1 Star Tags and above for Genres.
`anidb_0_5`Use AniDB Main Tags and All 0.5 Star Tags and above for Genres.
`mal`Use MyAnimeList for Genres.
`omdb`Use IMDb through OMDb for Genres. Requires [OMDB key](../config/omdb.md).
`anidb`Use AniDB Main Tags for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_3_0`Use AniDB Main Tags and All 3 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_2_5`Use AniDB Main Tags and All 2.5 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_2_0`Use AniDB Main Tags and All 2 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_1_5`Use AniDB Main Tags and All 1.5 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_1_0`Use AniDB Main Tags and All 1 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`anidb_0_5`Use AniDB Main Tags and All 0.5 Star Tags and above for Genres. Requires [AniDB clientID](../config/anidb.md).
`mal`Use MyAnimeList for Genres. Requires [MyAnimeList authentication](../config/myanimelist.md).
`lock`Lock all Genre Field.
`unlock`Unlock all Genre Field.
`remove`Remove all Genres and Lock all Field.
`reset`Remove all Genres and Unlock all Field.
List of Strings for Genres. (["String 1", "String 2"])
List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Strings for Genres. (["String 1", "String 2"])
???+ example "Example" @@ -142,7 +170,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: Movies: operations: - mass_genre_update: + mass_genre_update: - imdb - tmdb - ["Unknown"] @@ -153,33 +181,33 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_content_rating_update` - Updates the content rating of every item in the library."
Updates every item's content rating in the library to the chosen site's content rating. - +
- + **Attribute:** `mass_content_rating_update` - - **Accepted Values:** Source or List of sources to use in that order. - + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order. + ???+ tip "Note on `mdb` sources" - MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only - periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources + MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only + periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources directly. - - - - - - - + + + + + + + -
`mdb`Use MDBList for Content Ratings.
`mdb_commonsense`Use Common Sense Rating through MDBList for Content Ratings.
`mdb_commonsense0`Use Common Sense Rating with Zero Padding through MDBList for Content Ratings.
`mdb_age_rating`Use MDBList Age Rating for Content Ratings.
`mdb_age_rating0`Use MDBList Age Rating with Zero Padding for Content Ratings.
`omdb`Use IMDb through OMDb for Content Ratings.
`mal`Use MyAnimeList for Content Ratings.
`mdb`Use MDBList for Content Ratings. Requires [MDBList key](../config/mdblist.md).
`mdb_commonsense`Use Common Sense Rating through MDBList for Content Ratings. Requires [MDBList key](../config/mdblist.md).
`mdb_commonsense0`Use Common Sense Rating with Zero Padding through MDBList for Content Ratings. Requires [MDBList key](../config/mdblist.md).
`mdb_age_rating`Use MDBList Age Rating for Content Ratings. Requires [MDBList key](../config/mdblist.md).
`mdb_age_rating0`Use MDBList Age Rating with Zero Padding for Content Ratings. Requires [MDBList key](../config/mdblist.md).
`omdb`Use IMDb through OMDb for Content Ratings. Requires [OMDB key](../config/omdb.md).
`mal`Use MyAnimeList for Content Ratings. Requires [MyAnimeList authentication](../config/myanimelist.md).
`lock`Lock Content Rating Field.
`unlock`Unlock Content Rating Field.
`remove`Remove Content Rating and Lock Field.
`reset`Remove Content Rating and Unlock Field.
Any String for Content Ratings.
+ ???+ example "Example" @@ -187,7 +215,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: Movies: operations: - mass_content_rating_update: + mass_content_rating_update: - mdb_commonsense - mdb_age_rating - NR @@ -198,25 +226,25 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_original_title_update` - Updates the original title of every item in the library."
Updates every item's original title in the library to the chosen site's original title. - +
- + **Attribute:** `mass_original_title_update` - - **Accepted Values:** Source or List of sources to use in that order - + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order + - - - - - + + + + + -
`anidb`Use AniDB Main Title for Original Titles.
`anidb_official`Use AniDB Official Title based on the language attribute in the config file for Original Titles.
`mal`Use MyAnimeList Main Title for Original Titles.
`mal_english`Use MyAnimeList English Title for Original Titles.
`mal_japanese`Use MyAnimeList Japanese Title for Original Titles.
`anidb`Use AniDB Main Title for Original Titles. Requires [AniDB clientID](../config/anidb.md).
`anidb_official`Use AniDB Official Title based on the language attribute in the config file for Original Titles. Requires [AniDB clientID](../config/anidb.md).
`mal`Use MyAnimeList Main Title for Original Titles. Requires [MyAnimeList authentication](../config/myanimelist.md).
`mal_english`Use MyAnimeList English Title for Original Titles. Requires [MyAnimeList authentication](../config/myanimelist.md).
`mal_japanese`Use MyAnimeList Japanese Title for Original Titles. Requires [MyAnimeList authentication](../config/myanimelist.md).
`lock`Lock Original Title Field.
`unlock`Unlock Original Title Field.
`remove`Remove Original Title and Lock Field.
`reset`Remove Original Title and Unlock Field.
Any String for Original Titles.
+ ???+ example "Example" @@ -224,7 +252,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: Anime: operations: - mass_original_title_update: + mass_original_title_update: - anidb_official - anidb - Unknown @@ -235,23 +263,23 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_studio_update` - Updates the studio of every item in the library."
Updates every item's studio in the library to the chosen site's studio. - +
- + **Attribute:** `mass_studio_update` - - **Accepted Values:** Source or List of sources to use in that order - + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order + - - + + -
`anidb`Use AniDB Animation Work for Studio.
`mal`Use MyAnimeList Studio for Studio.
`anidb`Use AniDB Animation Work for Studio. Requires [AniDB clientID](../config/anidb.md).
`mal`Use MyAnimeList Studio for Studio. Requires [MyAnimeList authentication](../config/myanimelist.md).
`tmdb`Use TMDb Studio for Studio.
`lock`Lock Studio Field.
`unlock`Unlock Studio Field.
`remove`Remove Studio and Lock Field.
`reset`Remove Studio and Unlock Field.
Any String for Studio.
+ ???+ example "Example" @@ -259,7 +287,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: Anime: operations: - mass_studio_update: + mass_studio_update: - mal - anidb - Unknown @@ -270,37 +298,37 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_originally_available_update` - Updates the originally available date of every item in the library."
Updates every item's originally available date in the library to the chosen site's date. - + ???+ tip - + As plex does not allow this field to be empty, using `remove` or `reset` will set the date to the Plex default date, which is `1969-12-31`.
- + **Attribute:** `mass_originally_available_update` - - **Accepted Values:** Source or List of sources to use in that order. + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order. ???+ tip "Note on `mdb` sources" - MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only - periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources + MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only + periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources directly. - - - - - + + + + + -
`tmdb`Use TMDb Release Date.
`tvdb`Use TVDb Release Date.
`omdb`Use IMDb Release Date through OMDb.
`mdb`Use MDBList Release Date.
`mdb_digital`Use MDBList Digital Release Date.
`anidb`Use AniDB Release Date.
`mal`Use MyAnimeList Release Date.
`omdb`Use IMDb Release Date through OMDb. Requires [OMDB key](../config/omdb.md).
`mdb`Use MDBList Release Date. Requires [MDBList key](../config/mdblist.md).
`mdb_digital`Use MDBList Digital Release Date. Requires [MDBList key](../config/mdblist.md).
`anidb`Use AniDB Release Date. Requires [AniDB clientID](../config/anidb.md).
`mal`Use MyAnimeList Release Date. Requires [MyAnimeList authentication](../config/myanimelist.md).
`lock`Lock Originally Available Field.
`unlock`Unlock Originally Available Field.
`remove`Remove Originally Available and Lock Field.
`reset`Remove Originally Available and Unlock Field.
Any String in the Format: YYYY-MM-DD for Originally Available. (2022-05-28)
+ ???+ example "Example" @@ -308,7 +336,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: TV Shows: operations: - mass_originally_available_update: + mass_originally_available_update: - mdb_digital - mdb - 1900-01-01 @@ -321,31 +349,31 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Updates every item's added at date in the library to the chosen site's date.
- + **Attribute:** `mass_added_at_update` - - **Accepted Values:** Source or List of sources to use in that order. + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order. ???+ tip "Note on `mdb` sources" - MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only - periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources + MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only + periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources directly. - - - - - + + + + + -
`tmdb`Use TMDb Release Date.
`tvdb`Use TVDb Release Date.
`omdb`Use IMDb Release Date through OMDb.
`mdb`Use MDBList Release Date.
`mdb_digital`Use MDBList Digital Release Date.
`anidb`Use AniDB Release Date.
`mal`Use MyAnimeList Release Date.
`omdb`Use IMDb Release Date through OMDb. Requires [OMDB key](../config/omdb.md).
`mdb`Use MDBList Release Date. Requires [MDBList key](../config/mdblist.md).
`mdb_digital`Use MDBList Digital Release Date. Requires [MDBList key](../config/mdblist.md).
`anidb`Use AniDB Release Date. Requires [AniDB clientID](../config/anidb.md).
`mal`Use MyAnimeList Release Date. Requires [MyAnimeList authentication](../config/myanimelist.md).
`lock`Lock Added At Field.
`unlock`Unlock Added At Field.
`remove`Remove Added At and Lock Field.
`reset`Remove Added At and Unlock Field.
Any String in the Format: YYYY-MM-DD for Added At. (2022-05-28)
+ ???+ example "Example" @@ -353,7 +381,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: TV Shows: operations: - mass_added_at_update: + mass_added_at_update: - mdb_digital - mdb - 1900-01-01 @@ -364,60 +392,60 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_***_rating_update` - Updates the audience/critic/user rating of every item in the library."
Updates every item's audience/critic/user rating in the library to the chosen site's rating. - + ???+ warning "Important Note" - - This does not affect the icons displayed in the Plex UI. This will place the number of your choice in the relevant field in the Plex database. In other words, if Plex is - configured to use Rotten Tomatoes ratings, then no matter what happens with this mass rating update Operation, the icons in the Plex UI will remain Rotten Tomatoes. The - human who decided to put TMDb ratings in the critic slot and Letterboxd ratings in the audience slot is the only party who knows that the ratings are no longer Rotten - Tomatoes. One primary use of this feature is to put ratings overlays on posters. More information on what Kometa can do with these ratings can be found + + This does not affect the icons displayed in the Plex UI. This will place the number of your choice in the relevant field in the Plex database. In other words, if Plex is + configured to use Rotten Tomatoes ratings, then no matter what happens with this mass rating update Operation, the icons in the Plex UI will remain Rotten Tomatoes. The + human who decided to put TMDb ratings in the critic slot and Letterboxd ratings in the audience slot is the only party who knows that the ratings are no longer Rotten + Tomatoes. One primary use of this feature is to put ratings overlays on posters. More information on what Kometa can do with these ratings can be found [here](../kometa/guides/ratings.md).
- + **Attribute:** `mass_audience_rating_update`/`mass_critic_rating_update`/`mass_user_rating_update` - - **Accepted Values:** Source or List of sources to use in that order. + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order. ???+ tip "Note on `mdb` sources" - MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only - periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources + MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only + periodically refreshed. As such, the data that Kometa applies using `mdb_` operations applies may not be the same as you see if you visit those third-party sources directly. - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + -
`anidb_average`Use AniDB Average.
`anidb_rating`Use AniDB Rating.
`anidb_score`Use AniDB Review Score.
`anidb_average`Use AniDB Average. Requires [AniDB clientID](../config/anidb.md).
`anidb_rating`Use AniDB Rating. Requires [AniDB clientID](../config/anidb.md).
`anidb_score`Use AniDB Review Score. Requires [AniDB clientID](../config/anidb.md).
`imdb`Use IMDb Rating.
`mal`Use MyAnimeList Score.
`mdb_average`Use MDBList Average Score.
`mdb_imdb`Use IMDb Rating through MDBList.
`mdb_letterboxd`Use Letterboxd Rating through MDBList.
`mdb_metacritic`Use Metacritic Rating through MDBList.
`mdb_metacriticuser`Use Metacritic User Rating through MDBList.
`mdb_myanimelist`Use MyAnimeList Rating through MDBList.
`mdb_tmdb`Use TMDb Rating through MDBList.
`mdb_tomatoes`Use Rotten Tomatoes Rating through MDBList.
`mdb_tomatoesaudience`Use Rotten Tomatoes Audience Rating through MDBList.
`mdb_trakt`Use Trakt Rating through MDBList.
`mdb`Use MDBList Score.
`omdb_metascore`Use Metacritic Metascore through OMDb.
`omdb_tomatoes`Use Rotten Tomatoes rating through OMDb.
`omdb`Use IMDbRating through OMDb.
`mal`Use MyAnimeList Score. Requires [MyAnimeList authentication](../config/myanimelist.md).
`mdb_average`Use MDBList Average Score. Requires [MDBList key](../config/mdblist.md).
`mdb_imdb`Use IMDb Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_letterboxd`Use Letterboxd Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_metacritic`Use Metacritic Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_metacriticuser`Use Metacritic User Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_myanimelist`Use MyAnimeList Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_tmdb`Use TMDb Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_tomatoes`Use Rotten Tomatoes Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_tomatoesaudience`Use Rotten Tomatoes Audience Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb_trakt`Use Trakt Rating through MDBList. Requires [MDBList key](../config/mdblist.md).
`mdb`Use MDBList Score. Requires [MDBList key](../config/mdblist.md).
`omdb_metascore`Use Metacritic Metascore through OMDb. Requires [OMDB key](../config/omdb.md).
`omdb_tomatoes`Use Rotten Tomatoes rating through OMDb. Requires [OMDB key](../config/omdb.md).
`omdb`Use IMDbRating through OMDb. Requires [OMDB key](../config/omdb.md).
`plex_imdb`Use IMDB Rating through Plex.
`plex_tmdb`Use TMDB Rating through Plex.
`plex_tomatoes`Use Rotten Tomatoes Rating through Plex.
`plex_tomatoesaudience`Use Rotten Tomatoes Audience Rating through Plex.
`tmdb`Use TMDb Rating.
`trakt_user`Use Trakt User's Personal Rating.
`trakt`Use Trakt Rating.
`trakt_user`Use Trakt User's Personal Rating. Requires [Trakt authentication](../config/trakt.md).
`trakt`Use Trakt Rating. Requires [Trakt authentication](../config/trakt.md).
`lock`Lock Rating Field.
`unlock`Unlock Rating Field.
`remove`Remove Rating and Lock Field.
`reset`Remove Rating and Unlock Field.
Any Number between 0.0-10.0 for Ratings.
+ ???+ example "Example" @@ -425,7 +453,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: Movies: operations: - mass_audience_rating_update: + mass_audience_rating_update: - mdb - mdb_average - 2.0 @@ -433,7 +461,7 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req - imdb - omdb - 2.0 - mass_user_rating_update: + mass_user_rating_update: - trakt_user - 2.0 ``` @@ -443,33 +471,33 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_episode_***_rating_update` - Updates the audience/critic/user rating of every episode in the library."
Updates every item's episode's audience/critic/user rating in the library to the chosen site's rating. - + ???+ warning "Important Note" - - This does not affect the icons displayed in the Plex UI. This will place the number of your choice in the relevant field in the Plex database. In other words, if Plex is - configured to use Rotten Tomatoes ratings, then no matter what happens with this mass rating update Operation, the icons in the Plex UI will remain Rotten Tomatoes. The - human who decided to put TMDb ratings in the critic slot and Letterboxd ratings in the audience slot is the only party who knows that the ratings are no longer Rotten - Tomatoes. One primary use of this feature is to put ratings overlays on posters. More information on what Kometa can do with these ratings can be found + + This does not affect the icons displayed in the Plex UI. This will place the number of your choice in the relevant field in the Plex database. In other words, if Plex is + configured to use Rotten Tomatoes ratings, then no matter what happens with this mass rating update Operation, the icons in the Plex UI will remain Rotten Tomatoes. The + human who decided to put TMDb ratings in the critic slot and Letterboxd ratings in the audience slot is the only party who knows that the ratings are no longer Rotten + Tomatoes. One primary use of this feature is to put ratings overlays on posters. More information on what Kometa can do with these ratings can be found [here](../kometa/guides/ratings.md).
- + **Attribute:** `mass_episode_audience_rating_update`/`mass_episode_critic_rating_update`/`mass_episode_user_rating_update` - - **Accepted Values:** Source or List of sources to use in that order. - + + **Accepted Values:** Source or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of sources to use in that order. + - + -
`imdb`Use IMDb Rating.
`plex_imdb`Use IMDB Rating through Plex.
`plex_tmdb`Use TMDB Rating through Plex.
`tmdb`Use TMDb Rating.
`trakt`Use Trakt Rating.
`trakt`Use Trakt Rating. Requires [Trakt authentication](../config/trakt.md).
`lock`Lock Rating Field.
`unlock`Unlock Rating Field.
`remove`Remove Rating and Lock Field.
`reset`Remove Rating and Unlock Field.
Any Number between 0.0-10.0 for Ratings.
+ ???+ example "Example" @@ -477,10 +505,10 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req libraries: TV Shows: operations: - mass_episode_audience_rating_update: + mass_episode_audience_rating_update: - tmdb - 2.0 - mass_episode_critic_rating_update: + mass_episode_critic_rating_update: - imdb - 2.0 ``` @@ -489,26 +517,26 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_poster_update` - Updates the poster of every item in the library." -
Updates every item's poster to the chosen sites poster. Will fall back to `plex` if the given option fails. +
Updates every item's poster to the chosen sites poster. Will fall back to `plex` if the given option fails. Assets will be used over anything else. - + ???+ warning - - When used in combination with Overlays, this could cause Kometa to reset the poster and then reapply all overlays + + When used in combination with Overlays, this could cause Kometa to reset the poster and then reapply all overlays on each run, which will result in [image bloat](../kometa/scripts/imagemaid.md).
- + **Attribute:** `mass_poster_update` - + **Accepted Values:** - + - - + +
`source`Source of the poster update.`tmdb`, `plex`, `lock`, or `unlock`
`seasons`Update season posters while updating shows. **Default:** `true``true` or `false`
`episodes`Update episode posters while updating shows. **Default:** `true``true` or `false`
`ignore_locked`Skip updating image if the poster field is locked**1**.
**Default:** `false`
`true` or `false`
`ignore_overlays`Skip updating image if the current image has an Overlay**2**.
**Default:** `false`
`true` or `false`
`ignore_locked`Skip updating image if the poster field is locked :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-operations-1" }
**Default:** `false`
`true` or `false`
`ignore_overlays`Skip updating image if the current image has an Overlay :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-operations-2" }
**Default:** `false`
`true` or `false`
1. The poster field will be locked if a previous `mass_poster_update` was run or if an Overlay has been applied. @@ -530,27 +558,27 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`mass_background_update` - Updates the background of every item in the library." -
Updates every item's background to the chosen sites background. Will fall back to `plex` if the given option fails. +
Updates every item's background to the chosen sites background. Will fall back to `plex` if the given option fails. Assets will be used over anything else. - + ???+ warning - - When used in combination with Overlays, this could cause Kometa to reset the background and then reapply all + + When used in combination with Overlays, this could cause Kometa to reset the background and then reapply all overlays on each run, which will result in [image bloat](../kometa/scripts/imagemaid.md).
- + **Attribute:** `mass_background_update` - + **Accepted Values:** - + - +
`source`Source of the poster update.`tmdb`, `plex`, `lock`, or `unlock`
`seasons`Update season posters while updating shows. **Default:** `true``true` or `false`
`episodes`Update episode posters while updating shows. **Default:** `true``true` or `false`
`ignore_locked`Skip updating image if the poster field is locked.**1** **Default:** `false``true` or `false`
`ignore_locked`Skip updating image if the poster field is locked :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-operations-1" }
**Default:** `false`
`true` or `false`
- + 1. The background field will be locked if a previous `mass_background_update` was run. ???+ example "Example" @@ -572,18 +600,18 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Updates every item's labels in the library to match the IMDb Parental Guide.
- + **Attribute:** `mass_imdb_parental_labels` - + **Accepted Values:** - +
`none`Apply all Parental Labels with a value of `None`, `Mild`, `Moderate`, or `Severe`.
`mild`Apply all Parental Labels with a value of `Mild`, `Moderate`, or `Severe`.
`moderate`Apply all Parental Labels with a value of `Moderate` or `Severe`.
`severe`Apply all Parental Labels with a value of `Severe`.
- + ???+ example "Example" ```yaml @@ -600,18 +628,18 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Updates every Collection in your library to the specified Collection Mode.
- + **Attribute:** `mass_collection_mode` - + **Accepted Values:** - +
`default`Library default.
`hide`Hide Collection.
`hide_items`Hide Items in this Collection.
`show_items`Show this Collection and its Items.
- + ???+ example "Example" ```yaml @@ -628,11 +656,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Search though every track in a music library and replace any blank track titles with the tracks sort title.
- + **Attribute:** `update_blank_track_titles` - + **Accepted Values:** `true` or `false`. - + ???+ example "Example" ```yaml @@ -649,11 +677,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Search through every title and remove all ending parentheses in an items title if the title is not locked.
- + **Attribute:** `remove_title_parentheses` - + **Accepted Values:** `true` or `false`. - + ???+ example "Example" ```yaml @@ -670,11 +698,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Splits all duplicate items found in this library.
- + **Attribute:** `split_duplicates` - + **Accepted Values:** `true` or `false`. - + ???+ example "Example" ```yaml @@ -688,19 +716,19 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`radarr_add_all` - Adds every item in the library to Radarr." -
Adds every item in the library to Radarr. +
Adds every item in the library to Radarr. ???+ warning - - The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr + + The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr paths you can use the `plex_path` and `radarr_path` [Radarr](radarr.md) details to convert the paths.
- + **Attribute:** `radarr_add_all` - + **Accepted Values:** `true` or `false`. - + ???+ example "Example" ```yaml @@ -717,11 +745,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Removes every item from Radarr with the Tags given.
- + **Attribute:** `radarr_remove_by_tag` - - **Accepted Values:** List or comma separated string of tags. - + + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string of tags. + ???+ example "Example" ```yaml @@ -735,19 +763,19 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req ??? blank "`sonarr_add_all` - Adds every item in the library to Sonarr." -
Adds every item in the library to Sonarr. +
Adds every item in the library to Sonarr. ???+ warning - - The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr + + The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr paths you can use the `plex_path` and `sonarr_path` [Sonarr](sonarr.md) details to convert the paths.
- + **Attribute:** `sonarr_add_all` - + **Accepted Values:** `true` or `false`. - + ???+ example "Example" ```yaml @@ -764,11 +792,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Removes every item from Sonarr with the Tags given.
- + **Attribute:** `sonarr_remove_by_tag` - - **Accepted Values:** List or comma separated string of tags. - + + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string of tags. + ???+ example "Example" ```yaml @@ -785,46 +813,46 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Maps genres in your library to be changed to other genres.
- + **Attribute:** `genre_mapper` - + **Accepted Values:** Each attribute under `genre_mapper` is a separate mapping and has two parts. - +
`key`Genre you want mapped to the value`Action/Adventure, Action & Adventure` in the example below.
`value`What the genre will end up as`Action` in the example below.
???+ example "Example" - - This example will change go through every item in your library and change the genre `Action/Adventure` or + + This example will change go through every item in your library and change the genre `Action/Adventure` or `Action & Adventure` to `Action` and `Romantic Comedy` to `Comedy`. - + ```yaml libraries: Movies: # Metadata and Overlay files here operations: genre_mapper: - "Action/Adventure": Action + "Action/Adventure": Action "Action & Adventure": Action Romantic Comedy: Comedy ``` - + To just Remove a Genre without replacing it just set the Genre to nothing like this. - + ```yaml libraries: Movies: # Metadata and Overlay files here operations: genre_mapper: - "Action/Adventure": Action + "Action/Adventure": Action "Action & Adventure": Action Romantic Comedy: ``` - - The above example will change go through every item in your library and change the genre `Action/Adventure` or + + The above example will change go through every item in your library and change the genre `Action/Adventure` or `Action & Adventure` to `Action` and remove every instance of the Genre `Romantic Comedy`. ###### Content Rating Mapper @@ -834,46 +862,46 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req
Maps content ratings in your library to be changed to other content ratings.
- + **Attribute:** `content_rating_mapper` - + **Accepted Values:** Each attribute under `content_rating_mapper` is a separate mapping and has two parts. - +
`key`Content rating you want mapped to the value.`PG`, `PG-13` in the example below.
`value`What the content rating will end up as.`Y-10` in the example below.
???+ example "Example" - - This example will change go through every item in your library and change the content rating `PG` or `PG-13` to + + This example will change go through every item in your library and change the content rating `PG` or `PG-13` to `Y-10` and `R` to `Y-17`. - + ```yaml libraries: Movies: # Metadata and Overlay files here operations: content_rating_mapper: - PG: Y-10 + PG: Y-10 "PG-13": Y-10 R: Y-17 ``` - + To just Remove a content rating without replacing it just set the content rating to nothing like this. - + ```yaml libraries: Movies: # Metadata and Overlay files here operations: content_rating_mapper: - PG: Y-10 + PG: Y-10 "PG-13": Y-10 R: ``` - - The above example will change go through every item in your library and change the content rating + + The above example will change go through every item in your library and change the content rating `PG` or `PG-13` to `Y-10` and remove every instance of the content rating `R`. ###### Metadata Backup @@ -885,20 +913,20 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req If you point to an existing Metadata File then Kometa will sync the changes to the file, so you won't lose non plex changes in the file.
- + **Attribute:** `metadata_backup` - + **Accepted Values:** There are a few different options to determine how the `metadata_backup` works. - + - +
`path`Path to where the metadata will be saved/maintained.
**Default:** `<>_Metadata_Backup.yml in your config folder`
**Values:** Path to Metadata File.
`exclude`Exclude all listed attributes from being saved in the Collection File.
**Values:** `Comma-separated string or list of attributes`.
`exclude`Exclude all listed attributes from being saved in the Collection File.
**Values:** `Comma-separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of attributes`.
`sync_tags`All Tag Attributes will have the `.sync` option and blank attribute will be added to sync.
**Default:** `false`
**Values:** `true` or `false`.
`add_blank_entries`Will add a line for entries that have no metadata changes.
**Default:** `true`
**Values:** `true` or `false`.
???+ example "Example" - + ```yaml libraries: Movies: @@ -909,11 +937,11 @@ For example, MDBList's free tier is limited [at time of writing] to 1000 API req add_blank_entries: false ``` - The resulting Metadata File can be used like any other [Metadata File](../files/metadata.md) in Kometa, for example, + The resulting Metadata File can be used like any other [Metadata File](../files/metadata.md) in Kometa, for example, you can use it to apply metadata to a new library or to restore metadata to a library. ???+ example "Example" - + ```yaml libraries: Movies: diff --git a/docs/config/overview.md b/docs/config/overview.md index 9a67c6cd6..eebdef738 100644 --- a/docs/config/overview.md +++ b/docs/config/overview.md @@ -22,17 +22,17 @@ requirements for setup that can be found by clicking the links within the table Although most connectors are not required for core Kometa functionality, some (such as Trakt and MDBList) are commonly used for third-party lists, so we would recommend configuring This connector. -
- + Hover over the numbered icons for additional information + | Attribute | Required | |:-----------------------------------|:------------------------------------------------------------------------| - | [`plex`](plex.md)(1) | :fontawesome-solid-circle-check:{ .green } | + | [`plex`](plex.md):material-numeric-1-circle:{ .aqua title="This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page." } | :fontawesome-solid-circle-check:{ .green } | | [`tmdb`](tmdb.md) | :fontawesome-solid-circle-check:{ .green } | | [`libraries`](libraries.md) | :fontawesome-solid-circle-check:{ .green } | | [`playlist_files`](playlists.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`settings`](settings.md) | :fontawesome-solid-circle-xmark:{ .red } | - | [`webhooks`](webhooks.md)(2) | :fontawesome-solid-circle-xmark:{ .red } | - | [`tautulli`](tautulli.md)(3) | :fontawesome-solid-circle-xmark:{ .red } | + | [`webhooks`](webhooks.md):material-numeric-1-circle:{ .aqua title="This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page." } | :fontawesome-solid-circle-xmark:{ .red } | + | [`tautulli`](tautulli.md):material-numeric-1-circle:{ .aqua title="This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page." } | :fontawesome-solid-circle-xmark:{ .red } | | [`github`](github.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`omdb`](omdb.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`mdblist`](mdblist.md) | :fontawesome-solid-circle-xmark:{ .red } | @@ -40,19 +40,11 @@ requirements for setup that can be found by clicking the links within the table | [`gotify`](gotify.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`ntfy`](ntfy.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`anidb`](anidb.md) | :fontawesome-solid-circle-xmark:{ .red } | - | [`radarr`](radarr.md)(4) | :fontawesome-solid-circle-xmark:{ .red } | - | [`sonarr`](sonarr.md)(5) | :fontawesome-solid-circle-xmark:{ .red } | + | [`radarr`](radarr.md):material-numeric-1-circle:{ .aqua title="This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page." } | :fontawesome-solid-circle-xmark:{ .red } | + | [`sonarr`](sonarr.md):material-numeric-1-circle:{ .aqua title="This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page." } | :fontawesome-solid-circle-xmark:{ .red } | | [`trakt`](trakt.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`mal`](myanimelist.md) | :fontawesome-solid-circle-xmark:{ .red } | -
- - 1. This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page. - 2. This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page. - 3. This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page. - 4. This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page. - 5. This connector can be configured either at the root level of the Config File, or per-library – examples are available on the connector's page. - ## Configuration Template File Example The below in an extract of the `config.yml.template` and is the initial values that are set if you follow any of the installation guides. diff --git a/docs/config/radarr.md b/docs/config/radarr.md index 4b66bc643..1eabe0e85 100644 --- a/docs/config/radarr.md +++ b/docs/config/radarr.md @@ -53,7 +53,7 @@ radarr: | `monitor` | Monitor new movies when adding. | **`true`** or `false` | :fontawesome-solid-circle-xmark:{ .red } | | `availability` | Minimum availability for new movies. | **`announced`**, `cinemas`, `released`, `db` | :fontawesome-solid-circle-check:{ .green } | | `quality_profile` | Quality profile for new movies. | Name of Radarr profile or leave **blank** | :fontawesome-solid-circle-check:{ .green } | -| `tag` | Tag(s) for new movies. | List (1) or comma-separated tags or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | +| `tag` | Tag(s) for new movies. | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated tags or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | | `search` | Start a search after adding movies. | `true` or **`false`** | :fontawesome-solid-circle-xmark:{ .red } | | `plex_path` | Convert this part of the path to `radarr_path` (used with `add_existing`). | Path string or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | | `radarr_path` | Target path replacing `plex_path` (used with `add_existing`). | Path string or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/config/schedule.md b/docs/config/schedule.md index 08e2b7dd0..b0c8e40a3 100644 --- a/docs/config/schedule.md +++ b/docs/config/schedule.md @@ -61,7 +61,7 @@ The scheduling options are: mass_critic_rating_update: tmdb ``` - 1. If you run Kometa on any day apart from Sunday,\n this library will not be processed at all + 1. If you run Kometa on any day apart from Sunday, this library will not be processed at all ??? blank "Scheduling Collection, Playlist, and Metadata Files" diff --git a/docs/config/settings.md b/docs/config/settings.md index 741ee62b5..0cabe51bc 100644 --- a/docs/config/settings.md +++ b/docs/config/settings.md @@ -1,6 +1,6 @@ --- search: - boost: 5 + boost: 5 hide: - toc --- @@ -16,7 +16,7 @@ Examples of these settings include the ability to: * Create asset folders for collections so that custom posters can be stored for upload. * Use a custom repository as the base for all `git` Metadata files. -The settings attribute and attributes can be specified individually per library, or can be inherited from the global value if it has been set. +The settings attribute and attributes can be specified individually per library, or can be inherited from the global value if it has been set. If an attribute is specified at both the library and global level, then the library level attribute will take priority. There are some attributes which can be specified at the collection level using [Settings](../files/settings.md). @@ -31,30 +31,30 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`asset_depth` - Used to control the depth of search in the asset directory."
Specify how many folder levels to scan for an item within the asset directory. - + At each asset level, Kometa will look for either `medianame.ext` [such as Star Wars.png] or a dedicated folder containing `poster.ext`. - - i.e. `/Star Wars/poster.png` and `/Star Wars.png` are both asset depth 0, whilst `/Movies/Star Wars/poster.png` and + + i.e. `/Star Wars/poster.png` and `/Star Wars.png` are both asset depth 0, whilst `/Movies/Star Wars/poster.png` and `/Movies/Star Wars.png` are both asset level 1. - + ???+ tip - + `asset_folders` must be set to `true` for this to take effect. - + increasing the amount of levels to scan will reduce performance
- + **Attribute:** `asset_depth` **Levels with this Attribute:** Global/Library - + **Accepted Values:** Any Integer 0 or greater. **Default Value:** `0` ???+ example "Example" - + ```yaml settings: asset_depth: 2 @@ -65,26 +65,26 @@ The available setting attributes which can be set at each level are outlined bel
Specify the directories where assets (posters, backgrounds, etc) are located. - ???+ tip - + ???+ tip + Assets can be stored anywhere on the host system that Kometa has visibility of (i.e. if using docker, the directory must be mounted/visible to the docker container). - + ??? warning - + Kometa will not create asset directories. Asset directories you specify here need to exist already.
- + **Attribute:** `asset_directory` **Levels with this Attribute:** Global/Library - - **Accepted Values:** Any Directory or List of Directories. + + **Accepted Values:** Any Directory or List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Directories. **Default Value:** `[Directory containing YAML config]/assets` ???+ example "Example" - + ```yaml settings: asset_directory: config/movies @@ -92,7 +92,7 @@ The available setting attributes which can be set at each level are outlined bel ```yaml settings: - asset_directory: + asset_directory: - config/assets/movies - config/assets/collections ``` @@ -100,22 +100,22 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`asset_folders` - Used to control the asset directory folder structure." -
While `true`, Kometa will search the `asset_directory` for a dedicated folder per item vs while false will look for an image. - +
While `true`, Kometa will search the `asset_directory` for a dedicated folder per item vs while false will look for an image. + i.e. When `true` the example path would be `/Star Wars/poster.png` instead of `/Star Wars.png`.
- + **Attribute:** `asset_folders` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: asset_folders: true @@ -135,17 +135,17 @@ The available setting attributes which can be set at each level are outlined bel You will suffer from [image bloat](../kometa/scripts/imagemaid.md) and your Kometa runs will be longer than needed if you do not use a cache file.
- + **Attribute:** `cache` **Levels with this Attribute:** Global - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: cache: true @@ -157,17 +157,17 @@ The available setting attributes which can be set at each level are outlined bel
Set the number of days before each cache mapping expires and has to be re-cached.
- + **Attribute:** `cache_expiration` **Levels with this Attribute:** Global - + **Accepted Values:** Integer greater than 0. **Default Value:** `60` ???+ example "Example" - + ```yaml settings: cache_expiration: 30 @@ -188,22 +188,24 @@ The available setting attributes which can be set at each level are outlined bel * Any Item in a library that is running the `assets_for_all` Library Operation. + * Any Collection in a library that is running the `assets_for_all_collections` Library Operation. + * Any Item that has an Overlay applied to it. - * Any Item found by a Builder while the definition also has `item_assets: true` specified. + * Any Item found by a Builder while the definition also has `item_assets: true` specified.
- + **Attribute:** `create_asset_folders` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: create_asset_folders: true @@ -213,24 +215,24 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`custom_repo` - Used to set up the custom `repo` [file block type](files.md#location-types-and-paths)."
Specify where the `repo` attribute's base is when defining `collection_files`, `metadata_files`, `playlist_file` and `overlay_files`. - + ???+ note - - Ensure you are using the raw GitHub link (i.e. + + Ensure you are using the raw GitHub link (i.e. https://github.com/Kometa-Team/Community-Configs/tree/master/meisnate12)
- + **Attribute:** `custom_repo` **Levels with this Attribute:** Global - + **Accepted Values:** Link to repository base. **Default Value:** `None` ???+ example "Example" - + ```yaml settings: custom_repo: https://github.com/Kometa-Team/Community-Configs/tree/master/meisnate12 @@ -240,30 +242,30 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`default_collection_order` - Used to set the `collection_order` for every collection run."
Set the `collection_order` for every collection run by Kometa unless the collection has a specific `collection_order`. - + ???+ tip - + `custom` cannot be used if more than one Builder is being used for the collection (such as `imdb_list` and `trakt_list` within the same collection).
- + **Attribute:** `default_collection_order` **Levels with this Attribute:** Global/Library - + **Accepted Values:** - +
`release`Order Collection by Release Dates.
`alpha`Order Collection Alphabetically.
`custom`Order Collection Via the Builder Order.
[Any `plex_search` sort option](../files/builders/plex.md#sort-options).
[Any `plex_search` sort option](../files/builders/plex/sort-options.md).
**Default Value:** `None` ???+ example "Example" - + ```yaml settings: default_collection_order: release @@ -273,23 +275,23 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`delete_below_minimum` - Used to delete collections below `minimum_items`"
When a collection is run, delete the collection if it is below the minimum number specified by `minimum_items`. - + ???+ tip - + Relies on `minimum_items` being set to the desired integer.
- + **Attribute:** `delete_below_minimum` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: delete_below_minimum: true @@ -301,17 +303,17 @@ The available setting attributes which can be set at each level are outlined bel
If a collection is skipped due to it not being scheduled, delete the collection.
- + **Attribute:** `delete_not_scheduled` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: settings: @@ -321,26 +323,26 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`dimensional_asset_rename` - Used to automatically rename asset files based on their dimensions." -
Whilst searching for assets, scan the folders within the `asset_directory` and if an asset poster (i.e. `/ASSET_NAME/poster.ext`) was not found, - rename the first image found that has a height greater than or equal to its width to `poster.ext`. If an asset background (i.e. `/ASSET_NAME/background.ext`), +
Whilst searching for assets, scan the folders within the `asset_directory` and if an asset poster (i.e. `/ASSET_NAME/poster.ext`) was not found, + rename the first image found that has a height greater than or equal to its width to `poster.ext`. If an asset background (i.e. `/ASSET_NAME/background.ext`), rename the first image found that has a width greater than its height to `background.ext`. - + ???+ tip - + `asset_folders` must be set to `true` for this to take effect.
- + **Attribute:** `dimensional_asset_rename` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: dimensional_asset_rename: true @@ -349,21 +351,21 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`download_url_assets` - Used to download url images into the asset directory." -
Whilst searching for assets, download images set within Collection/Metadata/Playlist +
Whilst searching for assets, download images set within Collection/Metadata/Playlist files( i.e. images set by `url_poster` or `url_background`) into the asset folder if none are already present. - +
- + **Attribute:** `download_url_assets` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: download_url_assets: true @@ -372,24 +374,24 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`ignore_ids` - List of TMDb/TVDb IDs to ignore." -
Set a list or comma-separated string of TMDb/TVDb IDs to ignore in all collections. - +
Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs to ignore in all collections. + ???+ note - + This does not apply to `smart_filter` Collections.
- + **Attribute:** `ignore_ids` **Levels with this Attribute:** Global/Library/Collection/Playlist - - **Accepted Values:** List or comma-separated string of TMDb/TVDb IDs. + + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs. **Default Value:** `None` ???+ example "Example" - + ```yaml settings: ignore_ids: 572802,695721 @@ -398,24 +400,24 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`ignore_imdb_ids` - List of IMDb IDs to ignore." -
Set a list or comma-separated string of IMDb IDs to ignore in all collections. - +
Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs to ignore in all collections. + ???+ note - + Rhis does not apply to `smart_filter` Collections.
- + **Attribute:** `ignore_imdb_ids` **Levels with this Attribute:** Global/Library/Collection/Playlist - - **Accepted Values:** List or comma-separated string of IMDb IDs. + + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs. **Default Value:** `None` ???+ example "Example" - + ```yaml settings: ignore_imdb_ids: tt6710474,tt1630029 @@ -425,23 +427,23 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`item_refresh_delay` - Time to wait between each `item_refresh`."
Specify the number of seconds to wait between each `item_refresh` of every movie/show in a collection/playlist. - + ???+ note - + Useful if your Plex Media Server is having issues with high request levels.
- + **Attribute:** `item_refresh_delay` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** Any Integer 0 or greater. (value is in seconds) **Default Value:** `0` ???+ example "Example" - + ```yaml settings: item_refresh_delay: 5 @@ -453,17 +455,17 @@ The available setting attributes which can be set at each level are outlined bel
Set the minimum number of items that must be found in order to build or update a collection/playlist.
- + **Attribute:** `minimum_items` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** Integer greater than 0 **Default Value:** `1` ???+ example "Example" - + ```yaml settings: minimum_items: 5 @@ -472,21 +474,21 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`missing_only_released` - Used to filter unreleased items from missing lists." -
Whilst running a collection or playlist, when Kometa handles missing items to either report it to the user, +
Whilst running a collection or playlist, when Kometa handles missing items to either report it to the user, report it to a file, or send it to Radarr/Sonarr all unreleased items will be filtered out.
- + **Attribute:** `missing_only_released` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: missing_only_released: true @@ -496,23 +498,23 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`only_filter_missing` - Used to have the `filter` only apply to missing items."
Only items missing from a collection will be filtered. **Only specific filters can filter missing. See [Filters](../files/filters.md) for more information.** - + ???+ note - + This can be used to filter which missing media items get sent to Sonarr/Radarr.
- + **Attribute:** `only_filter_missing` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: only_filter_missing: true @@ -524,11 +526,11 @@ The available setting attributes which can be set at each level are outlined bel
Used to control the filetype used with overlay images. This setting will only be applied to images generated after the value is added to your config.
- + **Attribute:** `overlay_artwork_filetype` **Levels with this Attribute:** Global/Library - + **Accepted Values:** @@ -541,7 +543,7 @@ The available setting attributes which can be set at each level are outlined bel **Default Value:** `webp_lossy` ???+ example "Example" - + ```yaml settings: overlay_artwork_filetype: png @@ -553,17 +555,17 @@ The available setting attributes which can be set at each level are outlined bel
Used to control the JPG or Lossy WEBP quality used with overlay images. This setting will only be applied to images generated after the value is added to your config.
- + **Attribute:** `overlay_artwork_quality` **Levels with this Attribute:** Global/Library - + **Accepted Values:** Any Integer 1-100 [Values over 95 are not recommended and may result in excessive image size, perhaps too large to be uploaded to Plex. **Default Value:** `None` [when no value is provided the standard 90 is used.] ???+ example "Example" - + ```yaml settings: overlay_artwork_quality: 90 @@ -575,17 +577,17 @@ The available setting attributes which can be set at each level are outlined bel
Set `playlist_report` to true to print out a playlist report at the end of the log.
- + **Attribute:** `playlist_report` **Levels with this Attribute:** Global - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: playlist_report: true @@ -595,24 +597,24 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`playlist_sync_to_users` - Set the default playlist `sync_to_users`."
Set the default playlist `sync_to_users`. To Sync a playlist to only yourself leave `playlist_sync_to_users` blank. - + ???+ note - + sharing playlists with other users will not share any posters associated with the playlist, this is a Plex limitation.
- + **Attribute:** `playlist_sync_to_users` **Levels with this Attribute:** Global/Playlist - - **Accepted Values:** `all`, list of users, or comma-separated string of users. Leave blank to not sync playlists to other users. + + **Accepted Values:** `all`, list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of users, or comma-separated string of users. Leave blank to not sync playlists to other users. ???+ example "Example" - + ```yaml settings: - playlist_sync_to_users: + playlist_sync_to_users: - user1 - user2 ``` @@ -625,37 +627,37 @@ The available setting attributes which can be set at each level are outlined bel Standard priority is as follows: 1. `url_poster` - + 2. `file_poster` - + 3. `tmdb_poster` - + 4. `tvdb_poster` - + 5. Asset directory - + 6. `tmdb_person` - + 7. `tmdb_collection_details` - + 8+. all other `_details` methods - + So if you have a poster for "Some Collection" specified as a `url_poster` and *also* as an asset, the `url_poster` will win and the asset will be ignored. This setting pushes `asset_directory` to the top of the list, so the asset would win over teh `url_poster`.
- + **Attribute:** `prioritize_assets` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: prioritize_assets: true @@ -667,7 +669,7 @@ The available setting attributes which can be set at each level are outlined bel
Specify the location where `save_report` is saved.
- + **Attribute:** `report_path` **Levels with this Attribute:** Library @@ -677,7 +679,7 @@ The available setting attributes which can be set at each level are outlined bel **Default Value:** `[Directory containing YAML config]/[Library Mapping Name]_report.yml` ???+ example "Example" - + ```yaml settings: report_path: config/TV_missing_report.yml @@ -687,25 +689,25 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`run_again_delay` - Used to control the number of minutes to delay running `run_again` collections."
Set the number of minutes to delay running `run_again` collections after daily run is finished. - + For example, if a collection adds items to Sonarr/Radarr, the library can automatically re-run "X" amount of time later so that any downloaded items are processed. - + ???+ tip - + A collection is a `run_again` collection if it has the `run_again` [Setting](../files/settings.md) attribute set to true.
- + **Attribute:** `run_again_delay` **Levels with this Attribute:** Global - + **Accepted Values:** Any Integer 0 or greater. **Default Value:** `0` ???+ example "Example" - + ```yaml settings: run_again_delay: 5 @@ -717,17 +719,17 @@ The available setting attributes which can be set at each level are outlined bel
Specify the run order of the library components [Library Operations, Collection Files and Overlay Files]
- + **Attribute:** `run_order` **Levels with this Attribute:** Global/Library - - **Accepted Values:** List or comma-separated string which must include `operations`, `metadata` and `overlays` in any order. + + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string which must include `operations`, `metadata` and `overlays` in any order. **Default Value:** `operations,metadata,collections,overlays` ???+ example "Example" - + ```yaml settings: run_order: @@ -743,17 +745,17 @@ The available setting attributes which can be set at each level are outlined bel
Save a report of the items added, removed, filtered, or missing from collections to a YAML file in the same directory as the file run.
- + **Attribute:** `save_report` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: save_report: false @@ -765,17 +767,17 @@ The available setting attributes which can be set at each level are outlined bel
Whilst searching for assets, show or hide the `update not needed` messages.
- + **Attribute:** `show_asset_not_needed` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_asset_not_needed: true @@ -787,17 +789,17 @@ The available setting attributes which can be set at each level are outlined bel
List all items which have been filtered out of a collection or playlist. (i.e. if it doesn't meet the filter criteria)
- + **Attribute:** `show_filtered` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: show_filtered: true @@ -809,17 +811,17 @@ The available setting attributes which can be set at each level are outlined bel
While `show_missing` is true items missing from collections or playlists will be displayed.
- + **Attribute:** `show_missing` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_missing: false @@ -831,17 +833,17 @@ The available setting attributes which can be set at each level are outlined bel
Display missing asset warnings for items, collections, and playlists.
- + **Attribute:** `show_missing_assets` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_missing_assets: false @@ -852,7 +854,7 @@ The available setting attributes which can be set at each level are outlined bel
Whilst searching for assets, when scanning for assets for a TV Show, if an Episode Title Card is found (i.e. `/ASSET_NAME/S##E##.ext`), notify the user of any episodes which do not have an asset image. - + ???+ tip "Shows/Hides messages like these for episodes" "Asset Warning: No poster found for '{item_title}' in the assets folder '{directory}'" @@ -862,17 +864,17 @@ The available setting attributes which can be set at each level are outlined bel "\nMissing S##E## Title Card"
- + **Attribute:** `show_missing_episode_assets` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_missing_episode_assets: true @@ -881,9 +883,9 @@ The available setting attributes which can be set at each level are outlined bel ??? blank "`show_missing_season_assets` - Used to show any missing season assets." -
Whilst searching for assets, when scanning for assets for a TV Show, if Season posters are found +
Whilst searching for assets, when scanning for assets for a TV Show, if Season posters are found (i.e. `/ASSET_NAME/Season##.ext`), notify the user of any seasons which do not have an asset image. - + ???+ tip "Shows/Hides messages like these for seasons/albums" "Asset Warning: No poster found for '{item_title}' in the assets folder '{directory}'" @@ -893,17 +895,17 @@ The available setting attributes which can be set at each level are outlined bel "Missing Season {season_number} Poster"
- + **Attribute:** `show_missing_season_assets` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_missing_season_assets: true @@ -917,17 +919,17 @@ The available setting attributes which can be set at each level are outlined bel i.e. a `smart_filter` on the `genre` attribute will return all of the attributes within the specified library.
- + **Attribute:** `show_options` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: show_options: true @@ -939,17 +941,17 @@ The available setting attributes which can be set at each level are outlined bel
List all collections not configured in the current Kometa run at the end of each run.
- + **Attribute:** `show_unconfigured` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_unconfigured: false @@ -961,17 +963,17 @@ The available setting attributes which can be set at each level are outlined bel
List all items which have made it through the filters INTO a collection or playlist. (i.e. if it meets the filter criteria)
- + **Attribute:** `show_unfiltered` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:** `true` or `false`. **Default Value:** `false` ???+ example "Example" - + ```yaml settings: show_unfiltered: true @@ -983,17 +985,17 @@ The available setting attributes which can be set at each level are outlined bel
List all collections not managed by Kometa at the end of each run.
- + **Attribute:** `show_unmanaged` **Levels with this Attribute:** Global/Library - + **Accepted Values:** `true` or `false`. **Default Value:** `true` ???+ example "Example" - + ```yaml settings: show_unmanaged: false @@ -1005,11 +1007,11 @@ The available setting attributes which can be set at each level are outlined bel
Sets the `sync_mode` for collections and playlists. Setting the `sync_mode` directly in a collection or playlist definition will override the `sync_mode` for that definition.
- + **Attribute:** `sync_mode` **Levels with this Attribute:** Global/Library/Collection/Playlist - + **Accepted Values:**
@@ -1020,52 +1022,52 @@ The available setting attributes which can be set at each level are outlined bel **Default Value:** `append` ???+ example "Example" - + ```yaml settings: sync_mode: sync ``` ???+ tip "What does this mean?" - + You have a Trakt list of ten movies. You run Kometa and create a collection from the list. The collection contains those ten movies. Tomorrow the list contains a different ten movies. You run Kometa. - `sync_mode: sync` - Kometa syncs the collection with the list, so the collection still has ten movies, but they are the ones that are in the Trakt list today. + `sync_mode: sync` - Kometa syncs the collection with the list, so the collection still has ten movies, but they are the ones that are in the Trakt list today. The original ten have been removed from the collection. `sync_mode: append` - Kometa appends the ten new movies to the collection, which now has twenty movies in it. The next day five movies change in the list. You run Kometa. - `sync_mode: sync` - Kometa syncs the collection with the list, so the collection still has ten movies, the ones that are in the Trakt list today. + `sync_mode: sync` - Kometa syncs the collection with the list, so the collection still has ten movies, the ones that are in the Trakt list today. The five that are no longer in the Trakt list are removed from the collection. - `sync_mode: append` - Kometa appends the five new movies to the collection, which now has twenty-five movies in it. + `sync_mode: append` - Kometa appends the five new movies to the collection, which now has twenty-five movies in it. ??? blank "`tvdb_language` - Specify the language to query TVDb in."
Specify the language to query TVDb in. - + ???+ note - + If no language is specified or the specified language is not found then the original language is used.
- + **Attribute:** `tvdb_language` **Levels with this Attribute:** Global - + **Accepted Values:** [Any ISO 639-2 Language Code.](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) **Default Value:** `None` ???+ example "Example" - + ```yaml settings: tvdb_language: eng @@ -1077,12 +1079,12 @@ The below in an extract of the `config.yml.template` and is the initial values t ???+ tip - We suggest users review each of these settings and amend as necessary, these are just default values to get you + We suggest users review each of these settings and amend as necessary, these are just default values to get you started. ~~~yaml -settings: {% - include-markdown "../../config/config.yml.template" +settings: {% + include-markdown "../../config/config.yml.template" comments=false preserve-includer-indent=false start="settings:" diff --git a/docs/config/sonarr.md b/docs/config/sonarr.md index 882980f6c..354cab5cf 100644 --- a/docs/config/sonarr.md +++ b/docs/config/sonarr.md @@ -62,7 +62,7 @@ sonarr: | `language_profile` | Language profile for new shows. | Name of Sonarr profile or leave **blank**
Default is the first listed profile | :fontawesome-solid-circle-xmark:{ .red } | | `series_type` | Series type when adding new shows. | **`standard`**, `daily`, `anime` | :fontawesome-solid-circle-xmark:{ .red } | | `season_folder` | Whether to use season folders. | **`true`** or `false` | :fontawesome-solid-circle-xmark:{ .red } | -| `tag` | Default tags for new shows. | List (1) or comma-separated values, or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | +| `tag` | Default tags for new shows. | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated values, or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | | `search` | Start search for missing episodes when adding shows. | `true` or **`false`** | :fontawesome-solid-circle-xmark:{ .red } | | `cutoff_search` | Start search for unmet cutoff episodes. | `true` or **`false`** | :fontawesome-solid-circle-xmark:{ .red } | | `plex_path` | When using `add_existing` or `sonarr_add_all` Convert this part of the path to `sonarr_path`. | Path string or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/config/tmdb.md b/docs/config/tmdb.md index 7f3e5ef5f..a0d01809d 100644 --- a/docs/config/tmdb.md +++ b/docs/config/tmdb.md @@ -18,12 +18,12 @@ tmdb: cache_expiration: 60 ``` -| Attribute | Description | Allowed Values (default in **bold**) | Required | -|:----------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------|:------------------------------------------:| -| `apikey` | User TMDb V3 API key. | Any valid API key | :fontawesome-solid-circle-check:{ .green } | -| `language` | [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for user language. | Two-letter ISO code, e.g. **`en`** | :fontawesome-solid-circle-xmark:{ .red } | -| `region` | [ISO 3166-1 code](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) for user region.
Used with [TMDb Chart Builders](../files/builders/tmdb.md#tmdb-chart-builders). | Two-letter code or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | -| `cache_expiration` | Number of days before each cache mapping expires and is re-cached. | Integer, e.g. **`60`** | :fontawesome-solid-circle-xmark:{ .red } | +| Attribute | Description | Allowed Values (default in **bold**) | Required | +|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------|:------------------------------------------:| +| `apikey` | User TMDb V3 API key. | Any valid API key | :fontawesome-solid-circle-check:{ .green } | +| `language` | [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for user language. | Two-letter ISO code, e.g. **`en`** | :fontawesome-solid-circle-xmark:{ .red } | +| `region` | [ISO 3166-1 code](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) for user region.
Used with [TMDb Chart Builders](../files/builders/tmdb/overview.md#tmdb-chart-builders). | Two-letter code or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | +| `cache_expiration` | Number of days before each cache mapping expires and is re-cached. | Integer, e.g. **`60`** | :fontawesome-solid-circle-xmark:{ .red } | ## Important Notes diff --git a/docs/config/trakt.md b/docs/config/trakt.md index 786bce2b4..cdd20dec8 100644 --- a/docs/config/trakt.md +++ b/docs/config/trakt.md @@ -15,6 +15,7 @@ trakt: client_id: 1a2b3c4d5e6f7g8h9i client_secret: 1a12b23c34d45e56f6 pin: + force_refresh: false authorization: access_token: 4cc355t0k3nh3r3 token_type: Bearer @@ -29,9 +30,14 @@ trakt: | `client_id` | Trakt application client ID. | Any valid ID or leave **blank** | :fontawesome-solid-circle-check:{ .green } | | `client_secret` | Trakt application client secret. | Any valid secret or leave **blank** | :fontawesome-solid-circle-check:{ .green } | | `pin` | Trakt PIN. | PIN string or leave **blank** | :fontawesome-solid-circle-xmark:{ .red } | +| `force_refresh` | Refresh credentials on every run | 'true' or 'false' | :fontawesome-solid-circle-xmark:{ .red } | -*All other attributes will be filled in by Kometa as part of the authentication process** +*All other attributes will be filled in by Kometa as part of the authentication process* + +### Important Note on "Authentication Process": + +The Trakt authentication process is interactive; Kometa will display a URL in the console output and then wait for you to visit that URL in order to grant access and then paste in some additional information. In order for this to happen you need to run Kometa in an interactive mode, which can be fussy in some contexts (e.g., running Kometa in a Docker container on a NAS). For this reason, it is far simpler to use the form down below to perform these steps; it does all the same steps, but takes them out of the Kometa script execution. The form will produce a complete authentication block as shown above ready for you to copy-paste into your `config.yml`. ## Trakt Authentication @@ -57,4 +63,4 @@ To connect to Trakt.tv you must create a Trakt application and supply Kometa the {% include-markdown "./authentication.md" start="# Trakt and MyAnimeList Authentication" -%} \ No newline at end of file +%} diff --git a/docs/defaults/both/actor.md b/docs/defaults/both/actor.md index 9741a6fe5..d75dcec83 100644 --- a/docs/defaults/both/actor.md +++ b/docs/defaults/both/actor.md @@ -22,7 +22,7 @@ hide: {% include-markdown "./../../templates/defaults/base/collection/variables_header.md" rewrite-relative-urls=false %} {% include-markdown "./../../templates/variable_list.md" - include-tags="people-data|exclude|include|limit|sort_by|style|format|tmdb_birthday|tmdb_person_offset" + include-tags="people-data|exclude|include|limit|sort_by|format|tmdb_birthday|tmdb_person_offset" replace='{ "DYNAMIC_NAME": "Actors", "DYNAMIC_VALUE": "Actor Names", diff --git a/docs/defaults/both/collectionless.md b/docs/defaults/both/collectionless.md index bfe38859b..d8d51b07e 100644 --- a/docs/defaults/both/collectionless.md +++ b/docs/defaults/both/collectionless.md @@ -2,18 +2,30 @@ hide: - toc --- + {% include-markdown "./../../templates/defaults/base/collection/header.md" replace='{ "COLLECTION": "Collectionless", "CODE_NAME": "collectionless", - "LIBRARY_TYPE": "Movie, Show", - "DESCRIPTION": "create a [Collectionless collection](../../files/builders/plex.md#plex-collectionless) to help Show/Hide Movies/Shows properly in your library" + "LIBRARY_TYPE": "Movie, Show", + "DESCRIPTION": "create a [Collectionless collection](../../files/builders/plex/collectionless.md) to help Show/Hide Movies/Shows properly in your library" }' rewrite-relative-urls=false - end="" + end="" %} +{% + include-markdown "./../../templates/defaults/base/collection/header.md" + replace='{ + "CODE_NAME": "collectionless", + "LIBRARY_TYPE": "Movie, Show" + }' + start="" + end="" +%} + + Requirements: * This file needs to run last under `collection_files`. @@ -26,7 +38,7 @@ Requirements: | Collection | Description | |:-----------------|:---------------------------------------------------------------------------------------------------------------------------------------| -| `Collectionless` | [Collectionless collection](../../files/builders/plex.md#plex-collectionless) to help Show/Hide Movies/Shows properly in your library. | +| `Collectionless` | [Collectionless collection](../../files/builders/plex/collectionless) to help Show/Hide Movies/Shows properly in your library. | {% include-markdown "./../../templates/defaults/base/mid.md" replace='{"CODE_NAME": "collectionless"}' diff --git a/docs/defaults/both/streaming.md b/docs/defaults/both/streaming.md index 775427f54..ea0063dce 100644 --- a/docs/defaults/both/streaming.md +++ b/docs/defaults/both/streaming.md @@ -35,11 +35,11 @@ hide: Some logic is applied for specific regions to prevent collections appearing which do not exist in said region. -| Region | Key | Description | -|:-----------------|:----------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------| -| `CA` | `max`, `showtime` | These collections will not be created if the region is `CA` as these streaming services are part of the Crave streaming service in Canada. | -| any besides `CA` | `crave` | These collections will not be created if the region is not `CA` as these streaming services are Canada-focused. | -| any besides `GB` | `all4`, `channel4`, `hayu`, `now` | These collections will not be created if the region is not `GB` as these streaming services are UK-focused. | +| Region | Key | Description | +|:-----------------|:----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------| +| `CA` | `max`, `showtime` | These collections will not be created if the region is `CA` as these streaming services are part of the Crave streaming service in Canada. | +| any besides `CA` | `crave` | These collections will not be created if the region is not `CA` as these streaming services are Canada-focused. | +| any besides `GB` | `channel4`, `itvx`, `now` | These collections will not be created if the region is not `GB` as these streaming services are UK-focused. | {% include-markdown "./../../templates/snippets/white_style.md" replace='{"CODE_NAME": "streaming"}' %} {% include-markdown "./../../templates/defaults/base/mid.md" replace='{"CODE_NAME": "streaming"}' include-tags='all|movie|show' %} diff --git a/docs/defaults/both/universe.md b/docs/defaults/both/universe.md index eb24f5347..d11101f21 100644 --- a/docs/defaults/both/universe.md +++ b/docs/defaults/both/universe.md @@ -15,6 +15,7 @@ hide: {% include-markdown "./../../templates/snippets/separator_line.md" replace='{"SEPARATOR": "Universe"}' %} | `Alien / Predator` | `avp` | Collection of Movies in the Alien / Predator Universe | | `Arrowverse` | `arrow` | Collection of Movies in the The Arrow Universe | +| `Conjuring Universe | `conjuring | Collection of Movies in the Conjuring Universe | | `DC Animated Universe` | `dca` | Collection of Movies in the DC Animated Universe | | `DC Extended Universe` | `dcu` | Collection of Movies in the DC Extended Universe | | `Fast & Furious` | `fast` | Collection of Movies in the Fast & Furious Universe | diff --git a/docs/defaults/movie/content_rating_us.md b/docs/defaults/movie/content_rating_us.md index 7340662b7..5822a9b6e 100644 --- a/docs/defaults/movie/content_rating_us.md +++ b/docs/defaults/movie/content_rating_us.md @@ -18,11 +18,10 @@ hide: }' replace-tags='{ "title-sub": "**[This file has a Show Library Counterpart.](./../../../../show/content_rating_us)**", - "image": "![](../../../../assets/images/defaults/content_rating_us_movie.png)" + "image": "![](../../../../assets/images/defaults/posters/content_rating_us_movie.png)" }' rewrite-relative-urls=false %} - === "Default `include`" {% include-markdown "../../templates/snippets/no-copy.md" rewrite-relative-urls=false %} diff --git a/docs/defaults/movie/director.md b/docs/defaults/movie/director.md index 1aa91b20f..b79759f84 100644 --- a/docs/defaults/movie/director.md +++ b/docs/defaults/movie/director.md @@ -22,7 +22,7 @@ hide: {% include-markdown "./../../templates/defaults/base/collection/variables_header.md" rewrite-relative-urls=false %} {% include-markdown "./../../templates/variable_list.md" - include-tags="people-data|exclude|include|limit|sort_by|style|format|tmdb_birthday|tmdb_person_offset" + include-tags="people-data|exclude|include|limit|sort_by|format|tmdb_birthday|tmdb_person_offset" replace='{ "DYNAMIC_NAME": "Directors", "DYNAMIC_VALUE": "Director Names", diff --git a/docs/defaults/movie/producer.md b/docs/defaults/movie/producer.md index 420802b3d..c79586a14 100644 --- a/docs/defaults/movie/producer.md +++ b/docs/defaults/movie/producer.md @@ -22,7 +22,7 @@ hide: {% include-markdown "./../../templates/defaults/base/collection/variables_header.md" rewrite-relative-urls=false %} {% include-markdown "./../../templates/variable_list.md" - include-tags="people-data|exclude|include|limit|sort_by|style|format|tmdb_birthday|tmdb_person_offset" + include-tags="people-data|exclude|include|limit|sort_by|format|tmdb_birthday|tmdb_person_offset" replace='{ "DYNAMIC_NAME": "Producers", "DYNAMIC_VALUE": "Producer Names", diff --git a/docs/defaults/movie/writer.md b/docs/defaults/movie/writer.md index 683de7ba9..7397f8924 100644 --- a/docs/defaults/movie/writer.md +++ b/docs/defaults/movie/writer.md @@ -22,7 +22,7 @@ hide: {% include-markdown "./../../templates/defaults/base/collection/variables_header.md" rewrite-relative-urls=false %} {% include-markdown "./../../templates/variable_list.md" - include-tags="people-data|exclude|include|limit|sort_by|style|format|tmdb_birthday|tmdb_person_offset" + include-tags="people-data|exclude|include|limit|sort_by|format|tmdb_birthday|tmdb_person_offset" replace='{ "DYNAMIC_NAME": "Writers", "DYNAMIC_VALUE": "Writer Names", diff --git a/docs/defaults/overlay_variables.md b/docs/defaults/overlay_variables.md index 0e84f0ae0..efca2990d 100644 --- a/docs/defaults/overlay_variables.md +++ b/docs/defaults/overlay_variables.md @@ -30,28 +30,26 @@ Color values should be wrapped in quotes in the YAML, as the `#` denotes a comme File paths need to be valid in the context where Kometa is running; this is primarily an issue when running in docker, as Kometa inside the container cannot see host paths. -| Variable | Description & Values | -|:---------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `back_align` | **Description:** Controls the Alignment for the Text Overlay inside the backdrop. If `back_align` is not specified the Backdrop Centers the text.
**Values:** `left`, `right`, `center`, `top`, or `bottom` | -| `back_color` | **Description:** Controls the Backdrop Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`
`AA` is transparency; 00 [transparent] to FF [opaque] | -| `back_height` | **Description:** Controls the Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | -| `back_line_color` | **Description:** Controls the Backdrop Line Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or ` #RRGGBBAA``AA ` is transparency; 00 [transparent] to FF [opaque] | -| `back_line_width` | **Description:** Controls the Backdrop Line Width for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | -| `back_padding` | **Description:** Controls the Backdrop Padding for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | -| `back_radius` | **Description:** Controls the Backdrop Radius for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | -| `back_width` | **Description:** Controls the Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | -| `file_<>`**1** | **Description:** Controls the image associated with this key's Overlay to a local file.
**Values:** Filepath to Overlay Image | -| `file` | **Description:** Controls the images associated with all the Overlays to a local file.
**Values:** Filepath to Overlay Image | -| `git_<>`**1** | **Description:** Controls the image associated with this key's Overlay to the git repo.
**Values:** Git Path to Overlay Image | -| `git` | **Description:** Controls the images associated with all the Overlays to the git repo.
**Values:** Git Path to Overlay Image | -| `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.
**Values:** `left`, `center`, or `right` | -| `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | -| `repo_<>`**1** | **Description:** Controls the image associated with this key's Overlay to a custom repo.
**Values:** Repo Path to Overlay Image | -| `repo` | **Description:** Controls the images associated with all the Overlays to a custom repo.
**Values:** Repo Path to Overlay Image | -| `url_<>`**1** | **Description:** Controls the image associated with this key's Overlay to a url.
**Values:** URL to Overlay Image | -| `url` | **Description:** Controls the images associated with all the Overlays to a url.
**Values:** URL to Overlay Image | -| `use_<>`**1** | **Description:** Turns off individual Overlays in a Defaults File.
**Values:** `false` to turn off the overlay | -| `vertical_align` | **Description:** Controls the Vertical Alignment of the overlay.
**Values:** `top`, `center`, or `bottom` | -| `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | - -1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<>` with when calling. \ No newline at end of file +| Variable | Description & Values | +|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `back_align` | **Description:** Controls the Alignment for the Text Overlay inside the backdrop. If `back_align` is not specified the Backdrop Centers the text.
**Values:** `left`, `right`, `center`, `top`, or `bottom` | +| `back_color` | **Description:** Controls the Backdrop Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`
`AA` is transparency; 00 [transparent] to FF [opaque] | +| `back_height` | **Description:** Controls the Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | +| `back_line_color` | **Description:** Controls the Backdrop Line Color for the Text Overlay.
**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`
`AA` is transparency; 00 [transparent] to FF [opaque] | +| `back_line_width` | **Description:** Controls the Backdrop Line Width for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | +| `back_padding` | **Description:** Controls the Backdrop Padding for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | +| `back_radius` | **Description:** Controls the Backdrop Radius for the Text Overlay.
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | +| `back_width` | **Description:** Controls the Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | +| `file_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a local file.
**Values:** Filepath to Overlay Image | +| `file` | **Description:** Controls the images associated with all the Overlays to a local file.
**Values:** Filepath to Overlay Image | +| `git_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to the git repo.
**Values:** Git Path to Overlay Image | +| `git` | **Description:** Controls the images associated with all the Overlays to the git repo.
**Values:** Git Path to Overlay Image | +| `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.
**Values:** `left`, `center`, or `right` | +| `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | +| `repo_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a custom repo.
**Values:** Repo Path to Overlay Image | +| `repo` | **Description:** Controls the images associated with all the Overlays to a custom repo.
**Values:** Repo Path to Overlay Image | +| `url_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a url.
**Values:** URL to Overlay Image | +| `url` | **Description:** Controls the images associated with all the Overlays to a url.
**Values:** URL to Overlay Image | +| `use_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Turns off individual Overlays in a Defaults File.
**Values:** `false` to turn off the overlay | +| `vertical_align` | **Description:** Controls the Vertical Alignment of the overlay.
**Values:** `top`, `center`, or `bottom` | +| `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.
**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | diff --git a/docs/defaults/overlays.md b/docs/defaults/overlays.md index 31ff4dd2e..c9c4a43a6 100644 --- a/docs/defaults/overlays.md +++ b/docs/defaults/overlays.md @@ -82,7 +82,6 @@ libraries: overlay_files: - default: resolution # 1 - default: audio_codec # 2 - - default: mediastinger # 3 - default: ratings # 4, 5, 6 template_variables: rating1: user # 4 as this is user and mass_user_rating_update: mdb_tomatoes diff --git a/docs/defaults/overlays/aspect.md b/docs/defaults/overlays/aspect.md index 4ec980971..31e67594a 100644 --- a/docs/defaults/overlays/aspect.md +++ b/docs/defaults/overlays/aspect.md @@ -22,7 +22,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "aspect", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "aspect", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -78,7 +78,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "aspect"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Aspect Overlays" diff --git a/docs/defaults/overlays/audio_codec.md b/docs/defaults/overlays/audio_codec.md index 424fbffbf..aeb62db09 100644 --- a/docs/defaults/overlays/audio_codec.md +++ b/docs/defaults/overlays/audio_codec.md @@ -32,7 +32,7 @@ hide: {% include-markdown "./../../templates/snippets/standard_style.md" replace='{"CODE_NAME": "audio_codec"}' %} {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "audio_codec", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "audio_codec", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -72,7 +72,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "audio_codec"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Audio Codec Overlays" diff --git a/docs/defaults/overlays/commonsense.md b/docs/defaults/overlays/commonsense.md index b35e8de21..9bf5fb6af 100644 --- a/docs/defaults/overlays/commonsense.md +++ b/docs/defaults/overlays/commonsense.md @@ -6,7 +6,7 @@ hide: include-markdown "./../../templates/defaults/base/overlays/header.md" replace='{ "OVERLAY_NAME": "Common Sense Age Rating", - "CODE_NAME": "audio_codec", + "CODE_NAME": "commonsense", "OVERLAY_LEVEL": "Movie, Show, Season, Episode", "DESCRIPTION": "an overlay based on the Common Sense Age Rating on each item within your library" }' @@ -38,7 +38,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "commonsense", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "commonsense", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -75,7 +75,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "commonsense"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Common Sense Age Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_au.md b/docs/defaults/overlays/content_rating_au.md index e063ea7b4..d3ebf037f 100644 --- a/docs/defaults/overlays/content_rating_au.md +++ b/docs/defaults/overlays/content_rating_au.md @@ -26,7 +26,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_au", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_au", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -61,7 +61,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_au"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "AU Content Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_de.md b/docs/defaults/overlays/content_rating_de.md index 742bdd5c3..5ff762867 100644 --- a/docs/defaults/overlays/content_rating_de.md +++ b/docs/defaults/overlays/content_rating_de.md @@ -26,7 +26,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_de", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_de", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -61,7 +61,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_de"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "DE Content Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_nz.md b/docs/defaults/overlays/content_rating_nz.md index 49e88156b..e5de69534 100644 --- a/docs/defaults/overlays/content_rating_nz.md +++ b/docs/defaults/overlays/content_rating_nz.md @@ -31,7 +31,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_nz", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_nz", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -66,7 +66,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_nz"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "NZ Content Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_uk.md b/docs/defaults/overlays/content_rating_uk.md index 7b4a69d31..350c132fc 100644 --- a/docs/defaults/overlays/content_rating_uk.md +++ b/docs/defaults/overlays/content_rating_uk.md @@ -27,7 +27,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_uk", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_uk", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -62,7 +62,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_uk"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "UK Content Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_us_movie.md b/docs/defaults/overlays/content_rating_us_movie.md index 4e0dbf5e3..38ae521e3 100644 --- a/docs/defaults/overlays/content_rating_us_movie.md +++ b/docs/defaults/overlays/content_rating_us_movie.md @@ -25,7 +25,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_us_movie", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_us_movie", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie' %} ```yaml @@ -60,7 +60,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_us_movie"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "US Movie Content Rating Overlays" diff --git a/docs/defaults/overlays/content_rating_us_show.md b/docs/defaults/overlays/content_rating_us_show.md index 2d68cebad..596887e81 100644 --- a/docs/defaults/overlays/content_rating_us_show.md +++ b/docs/defaults/overlays/content_rating_us_show.md @@ -25,7 +25,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "content_rating_us_show", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "content_rating_us_show", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|show|episode|season' %} ```yaml @@ -69,7 +69,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "content_rating_us_show"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "US Show Content Rating Overlays" diff --git a/docs/defaults/overlays/direct_play.md b/docs/defaults/overlays/direct_play.md index 25474365e..af772d8a8 100644 --- a/docs/defaults/overlays/direct_play.md +++ b/docs/defaults/overlays/direct_play.md @@ -14,7 +14,7 @@ hide: %} {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "direct_play", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "direct_play", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode' %} ```yaml @@ -48,7 +48,7 @@ hide: rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "direct_play"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Direct Play Overlays" diff --git a/docs/defaults/overlays/episode_info.md b/docs/defaults/overlays/episode_info.md index 04e43d1cd..3db11b9e8 100644 --- a/docs/defaults/overlays/episode_info.md +++ b/docs/defaults/overlays/episode_info.md @@ -14,7 +14,7 @@ hide: %} {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "episode_info", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "episode_info", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|episode' %} ```yaml @@ -45,7 +45,7 @@ hide: end='' %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "episode_info"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Episode Info Overlays" diff --git a/docs/defaults/overlays/language_count.md b/docs/defaults/overlays/language_count.md index c04c76816..e192bc971 100644 --- a/docs/defaults/overlays/language_count.md +++ b/docs/defaults/overlays/language_count.md @@ -16,7 +16,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "language_count", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "language_count", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -53,7 +53,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "language_count"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Audio/Subtitle Language Count Overlays" diff --git a/docs/defaults/overlays/languages.md b/docs/defaults/overlays/languages.md index e585a531a..d6cbce3f9 100644 --- a/docs/defaults/overlays/languages.md +++ b/docs/defaults/overlays/languages.md @@ -117,7 +117,7 @@ audio/subtitle languages available on each item within your library" {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "languages", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "languages", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -162,7 +162,7 @@ audio/subtitle languages available on each item within your library" {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "languages"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Audio/Subtitle Language Flag Overlays" diff --git a/docs/defaults/overlays/mediastinger.md b/docs/defaults/overlays/mediastinger.md index d764414da..53a394284 100644 --- a/docs/defaults/overlays/mediastinger.md +++ b/docs/defaults/overlays/mediastinger.md @@ -14,7 +14,7 @@ hide: %} {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "mediastinger", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "mediastinger", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie' %} ```yaml @@ -45,7 +45,7 @@ hide: end='' %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "mediastinger"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "MediaStinger Overlays" diff --git a/docs/defaults/overlays/network.md b/docs/defaults/overlays/network.md index aece74b9c..1c64f24cd 100644 --- a/docs/defaults/overlays/network.md +++ b/docs/defaults/overlays/network.md @@ -15,7 +15,7 @@ hide: {% include-markdown "./../../templates/snippets/white_style.md" replace='{"styles/CODE_NAME": "overlays/network"}' exclude-tags='logo' %} {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "network", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "network", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|show|episode|season' %} ```yaml @@ -64,7 +64,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "network"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Network Overlays" diff --git a/docs/defaults/overlays/ratings.md b/docs/defaults/overlays/ratings.md index fbc17eb20..e129dfbb9 100644 --- a/docs/defaults/overlays/ratings.md +++ b/docs/defaults/overlays/ratings.md @@ -23,7 +23,7 @@ Recommendations: Use the [Mass * Rating Update Library Operation](../../config/o {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "network", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "ratings", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|ratings|movie|show|episode' %} ```yaml @@ -110,8 +110,7 @@ Recommendations: Use the [Mass * Rating Update Library Operation](../../config/o rewrite-relative-urls=false %} -{% - include-markdown "./../../templates/defaults/base/overlays/shared.md" +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "ratings"}' replace-tags='{ "title-sub": "These variables can be prepended with `rating1_`, `rating2_`, or `rating3_` to change that attribute on each rating individually.", "notes-sub": "**IMPORTANT**: To amend `horizontal_offset` and `vertical_offset` you **must** prepend the variable with `rating1_`, `rating2_`, or `rating3_`." diff --git a/docs/defaults/overlays/resolution.md b/docs/defaults/overlays/resolution.md index 235a160f1..2f5d20efc 100644 --- a/docs/defaults/overlays/resolution.md +++ b/docs/defaults/overlays/resolution.md @@ -106,7 +106,7 @@ This is not something you can enable or disable independently; it's an internal {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "resolution", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "resolution", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -130,7 +130,7 @@ This is not something you can enable or disable independently; it's an internal 4. Stops this resolution overlay applying 5. Assuming that this library only contains 4K Movies, we will want to disable all non-4K overlays from processing to increase performance - + ```yaml overlay_files: - default: status template_variables: @@ -161,7 +161,7 @@ This is not something you can enable or disable independently; it's an internal {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "resolution"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Resolution/Edition Overlays" diff --git a/docs/defaults/overlays/ribbon.md b/docs/defaults/overlays/ribbon.md index 1dd8cea1f..f3133539e 100644 --- a/docs/defaults/overlays/ribbon.md +++ b/docs/defaults/overlays/ribbon.md @@ -34,7 +34,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "ribbon", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "ribbon", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show' %} @@ -72,7 +72,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "ribbon"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Ribbon Overlays" diff --git a/docs/defaults/overlays/runtimes.md b/docs/defaults/overlays/runtimes.md index 421de9ac5..35db185b9 100644 --- a/docs/defaults/overlays/runtimes.md +++ b/docs/defaults/overlays/runtimes.md @@ -15,7 +15,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "runtimes", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "runtimes", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode' %} ```yaml @@ -53,7 +53,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "runtimes"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Runtimes Overlays" diff --git a/docs/defaults/overlays/status.md b/docs/defaults/overlays/status.md index f4853463f..75714fbab 100644 --- a/docs/defaults/overlays/status.md +++ b/docs/defaults/overlays/status.md @@ -19,7 +19,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "status", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "status", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|show' %} ```yaml @@ -57,7 +57,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "status"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Status Overlays" diff --git a/docs/defaults/overlays/streaming.md b/docs/defaults/overlays/streaming.md index ffea25ce9..185ffc857 100644 --- a/docs/defaults/overlays/streaming.md +++ b/docs/defaults/overlays/streaming.md @@ -14,7 +14,7 @@ hide: | Netflix | `netflix` | `160` | | Prime Video | `amazon` | `150` | | Disney+ | `disney` | `140` | -| Max | `max` | `130` | +| HBO Max | `hbomax` | `130` | | Crunchyroll | `Crunchyroll` | `120` | | YouTube | `youtube` | `110` | | Hulu | `hulu` | `100` | @@ -28,6 +28,7 @@ hide: | ITVX | `itvx` | `30` | | BET+ | `bet` | `20` | | hayu | `hayu` | `10` | +| tubi | `tubi` | `5` | ## Regional Variants @@ -37,11 +38,11 @@ Some logic is applied to allow for regional streaming service lists to be availa |:-----------------|:----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| | any besides `GB` | `channel4`, `itvx`, `hayu`, `now` | These overlays will not be used if the region is not `uk` as these streaming services are UK-focused | | any besides `CA` | `crave` | These overlays will not be used if the region is not `ca` as these streaming services are Canada-focused | -| `CA` | `max`, `showtime` | These overlays will not be used if the region is `ca` as these streaming services are part of the Crave streaming service in Canada | +| `CA` | `hbomax`, `showtime` | These overlays will not be used if the region is `ca` as these streaming services are part of the Crave streaming service in Canada | {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "streaming", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "streaming", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show' %} ```yaml @@ -82,7 +83,7 @@ Some logic is applied to allow for regional streaming service lists to be availa {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "streaming"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Streaming Overlays" diff --git a/docs/defaults/overlays/studio.md b/docs/defaults/overlays/studio.md index 53e873ee3..c7a4c90fc 100644 --- a/docs/defaults/overlays/studio.md +++ b/docs/defaults/overlays/studio.md @@ -7,7 +7,7 @@ hide: replace='{ "OVERLAY_NAME": "Studio", "CODE_NAME": "studio", - "OVERLAY_LEVEL": "Movie, Show", + "OVERLAY_LEVEL": "Movie, Show, Season, Episode", "DESCRIPTION": "an overlay based on the show studio on each item within your library" }' end='' @@ -21,7 +21,7 @@ Below is a screenshot of the alternative Bigger (`bigger`) style which can be se {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "studio", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "studio", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show' %} ```yaml @@ -80,7 +80,7 @@ Below is a screenshot of the alternative Bigger (`bigger`) style which can be se {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "studio"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Studio Overlays" diff --git a/docs/defaults/overlays/versions.md b/docs/defaults/overlays/versions.md index a9566f2cf..b20c5026d 100644 --- a/docs/defaults/overlays/versions.md +++ b/docs/defaults/overlays/versions.md @@ -15,7 +15,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "versions", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "versions", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -36,8 +36,8 @@ hide: replace='{ "HORIZONTAL_OFFSET": "`15`/`235`", "HORIZONTAL_ALIGN": "`right`/`center`", - "VERTICAL_OFFSET": "`1050`/`15`", - "VERTICAL_ALIGN": "`top`", + "VERTICAL_OFFSET": "`335`/`270`", + "VERTICAL_ALIGN": "`bottom`", "BACK_COLOR": "`#00000099`", "BACK_RADIUS": "`30`", "BACK_WIDTH": "`105`", @@ -52,7 +52,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" end="" replace='{"CODE_NAME": "versions"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Versions Overlays" diff --git a/docs/defaults/overlays/video_format.md b/docs/defaults/overlays/video_format.md index c64255a2a..dd4e4826d 100644 --- a/docs/defaults/overlays/video_format.md +++ b/docs/defaults/overlays/video_format.md @@ -23,7 +23,7 @@ hide: {% include-markdown "./../../templates/defaults/base/mid.md" - replace='{"CODE_NAME": "video_format", "collection_files": "overlay_files"}' + replace='{"CODE_NAME": "video_format", "collection_files": "overlay_files", "collections:": "overlays:"}' include-tags='all|movie|show|episode|season' %} ```yaml @@ -63,7 +63,7 @@ hide: {% include-markdown "./../../templates/variable_list.md" include-tags="sup1" rewrite-relative-urls=false %} -{% include-markdown "./../../templates/defaults/base/overlays/shared.md" %} +{% include-markdown "./../../templates/defaults/base/overlays/shared.md" replace='{"CODE_NAME": "video_format"}' %} {% include-markdown "./../../templates/defaults/base/values.md" rewrite-relative-urls=false %} === "Video Format Overlays" diff --git a/docs/defaults/playlist.md b/docs/defaults/playlist.md index 474f4ab0e..daa4ac51f 100644 --- a/docs/defaults/playlist.md +++ b/docs/defaults/playlist.md @@ -27,7 +27,7 @@ use the `libraries` template variable which is outlined in the [Template Variabl ## Config -The below YAML in your config.yml will create the collections: +The below YAML in your config.yml will create the playlists: ```yaml playlist_files: diff --git a/docs/defaults/separators.md b/docs/defaults/separators.md index 2f9cfa55a..32d276cdd 100644 --- a/docs/defaults/separators.md +++ b/docs/defaults/separators.md @@ -43,6 +43,7 @@ These are all the files that contain a separator | Studio Collections | `studio` | | Subtitle Language Collections | `subtitle_language` | | Universe Collections | `universe` | +| Composers Collections | `composer` | | Writers Collections | `writer` | | Year Collections | `year` | diff --git a/docs/defaults/show/content_rating_us.md b/docs/defaults/show/content_rating_us.md index 6782c2ad0..50cc36cb1 100644 --- a/docs/defaults/show/content_rating_us.md +++ b/docs/defaults/show/content_rating_us.md @@ -19,7 +19,7 @@ hide: }' replace-tags='{ "title-sub": "**[This file has a Movie Library Counterpart.](./../../../../movie/content_rating_us)**", - "image": "![](../../../../assets/images/defaults/content_rating_us_show.png)" + "image": "![](../../../../assets/images/defaults/posters/content_rating_us_show.png)" }' rewrite-relative-urls=false %} diff --git a/docs/files/arr.md b/docs/files/arr.md index f981c8882..0c278146e 100644 --- a/docs/files/arr.md +++ b/docs/files/arr.md @@ -41,62 +41,46 @@ tags: All the following attributes can override the global/library [Radarr](../config/radarr.md) attributes which are the default unless otherwise specified. -
- -| Attribute | Description & Values | -|:--------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `radarr_add_missing` | **Description:** Override Radarr `add_missing` attribute
**Values:** `true` or `false` | -| `radarr_add_existing` | **Description:** Override Radarr `add_existing` attribute
**Values:** `true` or `false` | -| `radarr_upgrade_existing` | **Description:** Override Radarr `upgrade_existing` attribute
**Values:** `true` or `false` | -| `radarr_monitor_existing` | **Description:** Override Radarr `monitor_existing` attribute
**Values:** `true` or `false` | -| `radarr_ignore_cache` | **Description:** Override Radarr `ignore_cache` attribute
**Values:** `true` or `false` | -| `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute
**Values:** Folder Path | -| `radarr_monitor` | **Description:** Override Radarr `monitor` attribute
**Values:** `movie`, `collection`, or `none` | -| `radarr_availability` | **Description:** Override Radarr `availability` attribute
**Values:** `announced`, `cinemas`, `released`, `db` | -| `radarr_quality` | **Description:** Override Radarr `quality_profile` attribute
**Values:** Radarr Quality Profile | -| `radarr_tag` | **Description:** Override Radarr `tag` attribute
**Values:** List (1) or comma-separated string of tags | -| `radarr_search` | **Description:** Override Radarr `search` attribute
**Values:** `true` or `false` | -| `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr
**Values:** List (2) or comma-separated string of tags | -| `item_radarr_tag.remove` | **Description:** Used to remove existing tags in Radarr for every movie found by the builders that's in Radarr
**Values:** List (3)or comma-separated string of tags | -| `item_radarr_tag.sync` | **Description:** Matches the tags in Radarr for every movie found by the builders that's in Radarr with the provided tags
**Values:** List (4) or comma-separated string of tags | - -
- -{% - include-markdown "../templates/snippets/tooltips/arr_radarr.md" -%} +| Attribute | Description & Values | +|:--------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `radarr_add_missing` | **Description:** Override Radarr `add_missing` attribute
**Values:** `true` or `false` | +| `radarr_add_existing` | **Description:** Override Radarr `add_existing` attribute
**Values:** `true` or `false` | +| `radarr_upgrade_existing` | **Description:** Override Radarr `upgrade_existing` attribute
**Values:** `true` or `false` | +| `radarr_monitor_existing` | **Description:** Override Radarr `monitor_existing` attribute
**Values:** `true` or `false` | +| `radarr_ignore_cache` | **Description:** Override Radarr `ignore_cache` attribute
**Values:** `true` or `false` | +| `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute
**Values:** Folder Path | +| `radarr_monitor` | **Description:** Override Radarr `monitor` attribute
**Values:** `movie`, `collection`, or `none` | +| `radarr_availability` | **Description:** Override Radarr `availability` attribute
**Values:** `announced`, `cinemas`, `released`, `db` | +| `radarr_quality` | **Description:** Override Radarr `quality_profile` attribute
**Values:** Radarr Quality Profile | +| `radarr_tag` | **Description:** Override Radarr `tag` attribute
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `radarr_search` | **Description:** Override Radarr `search` attribute
**Values:** `true` or `false` | +| `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_radarr_tag.remove` | **Description:** Used to remove existing tags in Radarr for every movie found by the builders that's in Radarr
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_radarr_tag.sync` | **Description:** Matches the tags in Radarr for every movie found by the builders that's in Radarr with the provided tags
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | ## Sonarr Definition Settings All the following attributes can override the global/library [Sonarr](../config/sonarr.md) attributes which are the default unless otherwise specified. -
- -| Attribute | Description & Values | -|:--------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `sonarr_add_missing` | **Description:** Override Sonarr `add_missing` attribute
**Values:** `true` or `false` | -| `sonarr_add_existing` | **Description:** Override Sonarr `add_existing` attribute
**Values:** `true` or `false` | -| `sonarr_upgrade_existing` | **Description:** Override Sonarr `upgrade_existing` attribute
**Values:** `true` or `false` | -| `sonarr_monitor_existing` | **Description:** Override Sonarr `monitor_existing` attribute
**Values:** `true` or `false` | -| `sonarr_ignore_cache` | **Description:** Override Sonarr `ignore_cache` attribute
**Values:** `true` or `false` | -| `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute
**Values:** Folder Path | -| `sonarr_monitor` | **Description:** Override Sonarr `monitor` attribute
**Values:** `all`, `future`, `missing`, `existing`, `pilot`, `first`, `latest`, `none` | -| `sonarr_quality` | **Description:** Override Sonarr `quality_profile` attribute
**Values:** Sonarr Quality Profile | -| `sonarr_language` | **Description:** Override Sonarr `language_profile` attribute
**Values:** Sonarr Language Profile | -| `sonarr_series` | **Description:** Override Sonarr `series_type` attribute
**Values:** `standard`, `daily`, `anime` | -| `sonarr_season` | **Description:** Override Sonarr `season_folder` attribute
**Values:** `true` or `false` | -| `sonarr_tag` | **Description:** Override Sonarr `tag` attribute
**Values:** List (1) or comma-separated string of tags | -| `sonarr_search` | **Description:** Override Sonarr `search` attribute
**Values:** `true` or `false` | -| `sonarr_cutoff_search` | **Description:** Override Sonarr `cutoff_search` attribute
**Values:** `true` or `false` | -| `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr
**Values:** List (2) or comma-separated string of tags | -| `item_sonarr_tag.remove` | **Description:** Used to remove existing tags in Sonarr for every series found by the builders that's in Sonarr
**Values:** List (3) or comma-separated string of tags | -| `item_sonarr_tag.sync` | **Description:** Matches the tags in Sonarr for every series found by the builders that's in Sonarr with the provided tags
**Values:** List (4) or comma-separated string of tags | - -
- -{% - include-markdown "../templates/snippets/tooltips/arr_radarr.md" -%} +| Attribute | Description & Values | +|:--------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `sonarr_add_missing` | **Description:** Override Sonarr `add_missing` attribute
**Values:** `true` or `false` | +| `sonarr_add_existing` | **Description:** Override Sonarr `add_existing` attribute
**Values:** `true` or `false` | +| `sonarr_upgrade_existing` | **Description:** Override Sonarr `upgrade_existing` attribute
**Values:** `true` or `false` | +| `sonarr_monitor_existing` | **Description:** Override Sonarr `monitor_existing` attribute
**Values:** `true` or `false` | +| `sonarr_ignore_cache` | **Description:** Override Sonarr `ignore_cache` attribute
**Values:** `true` or `false` | +| `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute
**Values:** Folder Path | +| `sonarr_monitor` | **Description:** Override Sonarr `monitor` attribute
**Values:** `all`, `future`, `missing`, `existing`, `pilot`, `first`, `latest`, `none` | +| `sonarr_quality` | **Description:** Override Sonarr `quality_profile` attribute
**Values:** Sonarr Quality Profile | +| `sonarr_language` | **Description:** Override Sonarr `language_profile` attribute
**Values:** Sonarr Language Profile | +| `sonarr_series` | **Description:** Override Sonarr `series_type` attribute
**Values:** `standard`, `daily`, `anime` | +| `sonarr_season` | **Description:** Override Sonarr `season_folder` attribute
**Values:** `true` or `false` | +| `sonarr_tag` | **Description:** Override Sonarr `tag` attribute
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `sonarr_search` | **Description:** Override Sonarr `search` attribute
**Values:** `true` or `false` | +| `sonarr_cutoff_search` | **Description:** Override Sonarr `cutoff_search` attribute
**Values:** `true` or `false` | +| `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_sonarr_tag.remove` | **Description:** Used to remove existing tags in Sonarr for every series found by the builders that's in Sonarr
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_sonarr_tag.sync` | **Description:** Matches the tags in Sonarr for every series found by the builders that's in Sonarr with the provided tags
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | ## Adding to Arr You can add items to Radarr/Sonarr in two different ways. @@ -112,7 +96,7 @@ When `radarr_add_missing`/`sonarr_add_missing` are true the items missing from t When `radarr_add_existing`/`sonarr_add_existing` are true the items that exist in the collection/playlist will be added to Radarr/Sonarr. If your Radarr/Sonarr has different file system mappings from your Plex use `radarr_path`/`sonarr_path` along with `plex_path` from your -[Radarr](../config/radarr.md)/[Sonarr](../config/sonarr.md)global config settings. +[Radarr](../config/radarr.md)/[Sonarr](../config/sonarr.md) global config settings. ### Radarr Add Settings diff --git a/docs/files/builders/anidb.md b/docs/files/builders/anidb.md deleted file mode 100644 index 004e9dfde..000000000 --- a/docs/files/builders/anidb.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -hide: - - toc ---- -# AniDB Builders - -You can find anime using the features of [AniDB.net](https://anidb.net/) (AniDB). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:------------------------------------|:------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`anidb_id`](#anidb-id) | Finds the anime specified by the AniDB ID. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`anidb_relation`](#anidb-relation) | Finds all anime in the relation graph of the specified AniDB ID. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`anidb_popular`](#anidb-popular) | Finds every anime in AniDB's [Popular Anime](https://anidb.net/latest/anime/popular/?h=1) list. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`anidb_tags`](#anidb-tag) | Finds every anime in a AniDB Tag. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - - -=== "AniDB ID" - - Finds the anime specified by the AniDB ID. - - The expected input is an AniDB ID or AniDB Anime URL. Multiple values are supported as either a list or a comma-separated string. - - ### Example AniDB ID Builder(s) - - ```yaml - collections: - Sword Art Online Shows: - anidb_id: 8692, 8691, 13494 - ``` - ```yaml - collections: - Sword Art Online Shows: - anidb_id: - - 8692 - - 8691 - - 13494 - ``` - ```yaml - collections: - Sword Art Online Shows: - anidb_id: https://anidb.net/anime/8692, https://anidb.net/anime/8691, https://anidb.net/anime/13494 - ``` - -=== "AniDB Relation" - - Finds all anime in the relation graph of the specified AniDB ID. - - To see the relation graph of an anime use: `https://anidb.net/anime//relation/graph` but replace `` with the AniDB ID you want to see therelations for. - - **Value:** The expected input is an AniDB ID, AniDB Anime URL, or AniDB Anime Relation URL. Multiple values are supported as either a list or a comma-separated string. - - ### Example AniDB Relation Builder(s) - - ```yaml - collections: - All Sword Art Online: - anidb_relation: 8692 - ``` - ```yaml - collections: - All Sword Art Online: - anidb_relation: https://anidb.net/anime/8692 - ``` - ```yaml - collections: - All Sword Art Online: - anidb_relation: https://anidb.net/anime/8692/relation/graph - ``` - -=== "AniDB Popular" - - Finds every anime in AniDB's [Popular Anime](https://anidb.net/latest/anime/popular/?h=1) list. - - The expected input is a single integer value of how much anime to query with a max of 30. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example AniDB Popular Builder(s) - - ```yaml - collections: - AniDB Popular: - anidb_popular: 30 - collection_order: custom - sync_mode: sync - ``` - -=== "AniDB Tag" - - Finds anime with the specified AniDB Tag the options are detailed below. - - | Attribute | Description | Required | Default | - |:----------|:--------------------------------------------------------------|:------------------------------------------:|:-------:| - | `tag` | AniDB Tag ID to search by | :fontawesome-solid-circle-check:{ .green } | N/A | - | `limit` | Number of Anime to query from AniDB (use 0 for all; max: 500) | :fontawesome-solid-circle-xmark:{ .red } | 0 | - - ### Example AniDB Tag Builder(s) - - ```yaml - collections: - Pirates Anime: - anidb_tag: - tag: 1700 - limit: 500 - sync_mode: sync - ``` - - To find a list of AniDB tags, go to the [AniDB Anime](https://anidb.net/tag) page. On the tag you want, copy the link and find the tag ID at the end of the url. diff --git a/docs/files/builders/anidb/id.md b/docs/files/builders/anidb/id.md new file mode 100644 index 000000000..fbe75747c --- /dev/null +++ b/docs/files/builders/anidb/id.md @@ -0,0 +1,30 @@ +--- +hide: + - toc +--- +# AniDB ID + +Finds the anime specified by the AniDB ID. + +The expected input is an AniDB ID or AniDB Anime URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example AniDB ID Builder(s) + +```yaml +collections: + Sword Art Online Shows: + anidb_id: 8692, 8691, 13494 +``` +```yaml +collections: + Sword Art Online Shows: + anidb_id: + - 8692 + - 8691 + - 13494 +``` +```yaml +collections: + Sword Art Online Shows: + anidb_id: https://anidb.net/anime/8692, https://anidb.net/anime/8691, https://anidb.net/anime/13494 +``` \ No newline at end of file diff --git a/docs/files/builders/anidb/overview.md b/docs/files/builders/anidb/overview.md new file mode 100644 index 000000000..67e2957c5 --- /dev/null +++ b/docs/files/builders/anidb/overview.md @@ -0,0 +1,14 @@ +--- +hide: + - toc +--- +# AniDB Builders + +You can find anime using the features of [AniDB.net](https://anidb.net/) (AniDB). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:--------------------------------|:------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`anidb_id`](id.md) | Finds the anime specified by the AniDB ID. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`anidb_relation`](relation.md) | Finds all anime in the relation graph of the specified AniDB ID. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`anidb_popular`](popular.md) | Finds every anime in AniDB's [Popular Anime](https://anidb.net/latest/anime/popular/?h=1) list. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`anidb_tag`](tag.md) | Finds every anime in a AniDB Tag. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/files/builders/anidb/popular.md b/docs/files/builders/anidb/popular.md new file mode 100644 index 000000000..ebcafac61 --- /dev/null +++ b/docs/files/builders/anidb/popular.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# AniDB Popular + +Finds every anime in AniDB's [Popular Anime](https://anidb.net/latest/anime/popular/?h=1) list. + +The expected input is a single integer value of how much anime to query with a max of 30. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example AniDB Popular Builder(s) + +```yaml +collections: + AniDB Popular: + anidb_popular: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/anidb/relation.md b/docs/files/builders/anidb/relation.md new file mode 100644 index 000000000..5466d1982 --- /dev/null +++ b/docs/files/builders/anidb/relation.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# AniDB Relation + +Finds all anime in the relation graph of the specified AniDB ID. + +To see the relation graph of an anime use: `https://anidb.net/anime//relation/graph` but replace `` with the AniDB ID you want to see the relations for. + +**Value:** The expected input is an AniDB ID, AniDB Anime URL, or AniDB Anime Relation URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example AniDB Relation Builder(s) + +```yaml +collections: + All Sword Art Online: + anidb_relation: 8692 +``` +```yaml +collections: + All Sword Art Online: + anidb_relation: https://anidb.net/anime/8692 +``` +```yaml +collections: + All Sword Art Online: + anidb_relation: https://anidb.net/anime/8692/relation/graph +``` \ No newline at end of file diff --git a/docs/files/builders/anidb/tag.md b/docs/files/builders/anidb/tag.md new file mode 100644 index 000000000..bc6df4ad8 --- /dev/null +++ b/docs/files/builders/anidb/tag.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# AniDB Tag + +Finds anime with the specified AniDB Tag the options are detailed below. + +| Attribute | Description | Required | Default | +|:----------|:--------------------------------------------------------------|:------------------------------------------:|:-------:| +| `tag` | AniDB Tag ID to search by | :fontawesome-solid-circle-check:{ .green } | N/A | +| `limit` | Number of Anime to query from AniDB (use 0 for all; max: 500) | :fontawesome-solid-circle-xmark:{ .red } | 0 | + +### Example AniDB Tag Builder(s) + +```yaml +collections: + Pirates Anime: + anidb_tag: + tag: 1700 + limit: 500 + sync_mode: sync +``` + +To find a list of AniDB tags, go to the [AniDB Anime](https://anidb.net/tag) page. On the tag you want, copy the link and find the tag ID at the end of the url. diff --git a/docs/files/builders/anilist.md b/docs/files/builders/anilist.md index f4a49abb2..3e4c6f338 100644 --- a/docs/files/builders/anilist.md +++ b/docs/files/builders/anilist.md @@ -79,12 +79,12 @@ You can find anime using the features of [AniList.co](https://anilist.co/) (AniL ???+ tip "Number Attribute Modifiers" - | Number Modifier | Description | - |:----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| - | `.gt` | Matches every item where the number attribute is greater than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | - | `.gte` | Matches every item where the number attribute is greater than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | - | `.lt` | Matches every item where the number attribute is less than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | - | `.lte` | Matches every item where the number attribute is less than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | Number Modifier | Description | + |:----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| + | `.gt` | Matches every item where the number attribute is greater than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.gte` | Matches every item where the number attribute is greater than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.lt` | Matches every item where the number attribute is less than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.lte` | Matches every item where the number attribute is less than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | === "Tag Attributes" diff --git a/docs/files/builders/anilist/id.md b/docs/files/builders/anilist/id.md new file mode 100644 index 000000000..7733f2c2e --- /dev/null +++ b/docs/files/builders/anilist/id.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# AniList ID + +Finds the anime specified by the AniList ID. + +The expected input is an AniList ID. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example AniList ID Builder(s) + +```yaml +collections: + Cowboy Bebop: + anilist_id: 23, 219 +``` \ No newline at end of file diff --git a/docs/files/builders/anilist/overview.md b/docs/files/builders/anilist/overview.md new file mode 100644 index 000000000..0b86fb315 --- /dev/null +++ b/docs/files/builders/anilist/overview.md @@ -0,0 +1,18 @@ +--- +hide: + - toc +--- +# AniList Builders + +You can find anime using the features of [AniList.co](https://anilist.co/) (AniList). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:------------------------------------|:-------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`anilist_search`](search.md) | Finds the anime specified by the AniList search parameters provided | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`anilist_top_rated`](top-rated.md) | Finds every anime in AniList's [Top Rated Anime](https://anilist.co/search/anime?sort=SCORE_DESC) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`anilist_popular`](popular.md) | Finds every anime in AniList's [Popular Anime](https://anilist.co/search/anime/popular) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`anilist_trending`](trending.md) | Finds every anime in AniList's [Trending Anime](https://anilist.co/search/anime/trending) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`anilist_relations`](relations.md) | Finds the anime specified by the AniList ID and every relation in its relation tree except Character and Other relations | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`anilist_studio`](studio.md) | Finds all anime specified by the AniList Studio ID | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`anilist_id`](id.md) | Finds the anime specified by the AniList ID | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`anilist_userlist`](userlist.md) | Finds the anime in AniList User's Anime list the options are detailed below | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | diff --git a/docs/files/builders/anilist/popular.md b/docs/files/builders/anilist/popular.md new file mode 100644 index 000000000..acaa54633 --- /dev/null +++ b/docs/files/builders/anilist/popular.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# AniList Popular + +Finds every anime in AniList's [Popular Anime](https://anilist.co/search/anime/popular) list. + +The expected input is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example AniList Popular Builder(s) + +```yaml +collections: + Popular Anime: + anilist_popular: 10 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/anilist/relations.md b/docs/files/builders/anilist/relations.md new file mode 100644 index 000000000..176c31863 --- /dev/null +++ b/docs/files/builders/anilist/relations.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# AniList Relations + +Finds the anime specified by the AniList ID and every relation in its relation tree except Character and Other relations. + +The expected input is an AniList ID. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example AniList Relations Builder(s) + +```yaml +collections: + One Piece: + anilist_relations: 21 +``` \ No newline at end of file diff --git a/docs/files/builders/anilist/search.md b/docs/files/builders/anilist/search.md new file mode 100644 index 000000000..969346ba6 --- /dev/null +++ b/docs/files/builders/anilist/search.md @@ -0,0 +1,139 @@ +--- +hide: + - toc +--- +# AniList Search + +Finds the anime specified by the AniList Search the options are detailed below. + +There are three fields per search option when using AniList's Search just like Plex's Advanced Filters in the Web UI. The first is the **Attribute** (what attribute you wish to search), the second is the **Modifier** (which modifier to use), and the third is the **Term** (actual term to search). + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +=== "Special Attributes" + + Special attributes do not support any modifiers. + + | Special Attribute | Description & Values | + |:------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `sort_by` | **Description:** How to sort the Anime
**Default:** `score`
**Values:**
`score`Sort by Average Score
`popular`Sort by Popularity
`trending`Sort by Trending
| + | `limit` | **Description:** Number of Anime to query
**Values:** Number greater or equal to `0` (use 0 or don't use it at all for all anime)
**Default:** `0` | + | `search` | **Description:** Text to search
**Values:** Any Text | + | `season` | **Description:** Season to search for
**Default:** `current`
**Values:**
`winter`For winter season December, January, February
`spring`For spring season March, April, May
`summer`For summer season June, July, August
`fall`For fall season September, October, November
`current`For current Season
| + | `year` | **Description:** Season year to search for
**Default:** Current Year
**Values:** Number between `1917` and next year or leave blank for the current year | + | `min_tag_percent` | **Description:** Minimum tag percentage for the Anime
**Values:** Number between `0`-`100` | + | `adult` | **Description:** Search for or not for Adult Anime
**Values:** `true` or `false` | + | `country` | **Description:** Search for anime from a specific country
**Values:** [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) | + | `source` | **Description:** Uses the anime's source to match
**Values:** `original`, `manga`, `light_novel`, `visual_novel`, `video_game`, `other`, `novel`, `doujinshi`, or `anime` | + +=== "Date Attributes" + + Date attributes can be used with either `.before`, or `.after`. + + No date attribute can take multiple values. + + ### "Date Attributes" + + | Date Attributes | Description | + |:----------------|:---------------------------------------------| + | `start` | Uses the anime start date attribute to match | + | `end` | Uses the anime end date attribute to match | + + ???+ tip "Date Attribute Modifiers" + + | Date Modifier | Description | + |:--------------|:-----------------------------------------------------------------------------------------------------------------| + | `.before` | Matches every item where the date attribute is before the given date
**Format:** MM/DD/YYYY e.g. `01/01/2000` | + | `.after` | Matches every item where the date attribute is after the given date
**Format:** MM/DD/YYYY e.g. `01/01/2000` | + +=== "Number Searches" + + Number attributes must use `.gt`, `.gte`, `.lt`, or `.lte` as a modifier. + + No number attribute can take multiple values. + + ### "Number Attributes" + + | Number Attribute | Description | + |:-----------------|:------------------------------------------------------------------------------------------------------| + | `duration` | **Description:** Uses the duration attribute to match using minutes
**Restrictions:** minimum: `1` | + | `episodes` | **Description:** Uses the number of episodes attribute to match
**Restrictions:** minimum: `1` | + | `score` | **Description:** Uses the score attribute to match
**Restrictions:** minimum: `1` | + | `popularity` | **Description:** Uses the popularity attribute to match
**Restrictions:** minimum: `1` | + + ???+ tip "Number Attribute Modifiers" + + | Number Modifier | Description | + |:----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| + | `.gt` | Matches every item where the number attribute is greater than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.gte` | Matches every item where the number attribute is greater than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.lt` | Matches every item where the number attribute is less than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + | `.lte` | Matches every item where the number attribute is less than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | + + +=== "Tag Attributes" + + Tag attributes can be used with either no modifier or with `.not`. + + String attributes can take multiple values as a **list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string**. + + ### "Tag Attributes" + + | Tag Attribute | Description & Values | + |:---------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | `format` | **Description:** Uses the anime's format to match
**Values:** `tv`, `short`, `movie`, `special`, `ova`, `ona`, `music` | + | `status` | **Description:** Uses the anime's status to match
**Values:** `finished`, `airing`, `not_yet_aired`, `cancelled`, `hiatus` | + | `genre` | **Description:** Uses the anime's genre to match
**Values:** Any Genre in the Genre Dropdown box on the [AniList Search Page](https://anilist.co/search/anime) | + | `tag` | **Description:** Uses the anime's tag to match
**Values:** Any Tag in the Genre Dropdown box on the [AniList Search Page](https://anilist.co/search/anime) | + | `tag_category` | **Description:** Uses the anime's tag category to match
**Values:** Any Tag Category in the Advanced Genres & Tag Filters Menu on the [AniList Search Page](https://anilist.co/search/anime) | + + ???+ tip "Tag Attribute Modifiers" + + | Tag Modifier | Description | + |:-------------|:-----------------------------------------------------------------------| + | No Modifier | Matches every item where the attribute matches the given string | + | `.not` | Matches every item where the attribute does not match the given string | + + + +### Example AniList Search Builder(s) + +```yaml +collections: + Current Anime Season: + anilist_search: + season: + year: + sort_by: popular + collection_order: custom + sync_mode: sync +``` +```yaml +collections: + Fall 2020 Anime: + anilist_search: + season: fall + year: 2020 + collection_order: custom + sync_mode: sync +``` +```yaml +collections: + Pirates Anime: + anilist_search: + tag: Pirates + sort_by: popular + collection_order: custom + sync_mode: sync +``` +```yaml +collections: + Top Sports Anime: + anilist_genre: + genre: Sports + limit: 20 + sort_by: popular + collection_order: custom + sync_mode: sync +``` + diff --git a/docs/files/builders/anilist/studio.md b/docs/files/builders/anilist/studio.md new file mode 100644 index 000000000..d444443cb --- /dev/null +++ b/docs/files/builders/anilist/studio.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# AniList Studio + +Finds all anime specified by the AniList Studio ID. + +The expected input is an AniList ID. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example AniList Studio Builder(s) + +```yaml +collections: + Studio Ghibli: + anilist_studio: 21 +``` \ No newline at end of file diff --git a/docs/files/builders/anilist/top-rated.md b/docs/files/builders/anilist/top-rated.md new file mode 100644 index 000000000..c77ddd052 --- /dev/null +++ b/docs/files/builders/anilist/top-rated.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# AniList Top Rated + +Finds every anime in AniList's [Top Rated Anime](https://anilist.co/search/anime?sort=SCORE_DESC) list. + +The expected input is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example AniList Top Rated Builder(s) + +```yaml +collections: + Top Rated Anime: + anilist_top_rated: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/anilist/trending.md b/docs/files/builders/anilist/trending.md new file mode 100644 index 000000000..5db9cbcaa --- /dev/null +++ b/docs/files/builders/anilist/trending.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# AniList Trending + +Finds every anime in AniList's [Trending Anime](https://anilist.co/search/anime/trending) list. + +The expected input is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example AniList Trending Builder(s) + +```yaml +collections: + Trending Anime: + anilist_trending: 10 + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/anilist/userlist.md b/docs/files/builders/anilist/userlist.md new file mode 100644 index 000000000..382b401cc --- /dev/null +++ b/docs/files/builders/anilist/userlist.md @@ -0,0 +1,35 @@ +--- +hide: + - toc +--- +# AniList UserList + +Gets anime in AniList User's Anime list. The different sub-attributes are detailed below. + +Both `username` and `list_name` are required. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +| Attribute | Description | +|:------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `username` | **Description:** A user's AniList Username | +| `list_name` | **Description:** A user's AniList List Name | +| `score.gt` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-score-filters" } | **Description:** Only return items that have a score greater than the given number.
**Values:** `0.0`-`10.0` | +| `score.gte` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-score-filters" } | **Description:** Only return items that have a score greater than or equal to the given number.
**Values:** `0.0`-`10.0` | +| `score.lt` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-score-filters" } | **Description:** Only return items that have a score less than the given number.
**Values:** `0.0`-`10.0` | +| `score.lte` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-score-filters" } | **Description:** Only return items that have a score less than or equal to the given number.
**Values:** `0.0`-`10.0` | +| `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by User Score
`popularity`Sort by Popularity
`status`Sort by Status
`progress`Sort by Progress
`last_updated`Sort by Last Updated
`last_added`Sort by Last Added
`start_date`Sort by Start Date
`completed_date`Sort by Completed Date
| + +### Example AniList UserList Builder(s) + +```yaml +collections: + Currently Watching Anime: + anilist_userlist: + username: Username + list_name: Watching + sort_by: score + collection_order: custom + sync_mode: sync +``` + diff --git a/docs/files/builders/boxofficemojo/all-time.md b/docs/files/builders/boxofficemojo/all-time.md new file mode 100644 index 000000000..14f3ec994 --- /dev/null +++ b/docs/files/builders/boxofficemojo/all-time.md @@ -0,0 +1,41 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo All Time + +Uses the [All Time Lists](https://www.boxofficemojo.com/charts/overall/) to collect items. + +**Builder Attribute:** `mojo_all_time` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| `chart` | Determines the chart you want to use.
**Allowed Values:** `domestic` or `worldwide` | +| `content_rating_filter` | Determines the content rating chart to use.
**Allowed Values:** `g`, `g/pg`, `pg`, `pg-13`, `r`, or `nc-17` | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +### Example Mojo All Time Builder(s) + +```yaml +collections: + + Top 100 Domestic All Time Grosses: + mojo_all_time: + chart: domestic + limit: 100 + + Top 100 Worldwide All Time Grosses: + mojo_all_time: + chart: worldwide + limit: 100 + + Top 10 Domestic All Time G Movie Grosses: + mojo_all_time: + chart: domestic + content_rating_filter: g + limit: 10 +``` diff --git a/docs/files/builders/boxofficemojo/domestic.md b/docs/files/builders/boxofficemojo/domestic.md new file mode 100644 index 000000000..35f585023 --- /dev/null +++ b/docs/files/builders/boxofficemojo/domestic.md @@ -0,0 +1,54 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo Domestic + +Uses the Domestic Box Office to collect items. + +**Builder Attribute:** `mojo_domestic` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `range` | Determines the type of time range of the Box Office.
**Allowed Values:** `daily`, `weekend`, `weekly`, `monthly`, `quarterly`, `yearly`, `season`, or `holiday` | +| `year` | Determines the year of the Box Office. This attribute is ignored for the `daily` range.
**Default Value:** `current`
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | +| `range_data` | Determines the actual time range of the Box Office. The input changes depending on the value of `range`.
**Required:** Yes, except for `yearly` range | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +## Allowed Values for `range_data` + +| Range | Allowed Values | +|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `daily` | Date in the format `MM-DD-YYYY`, `current`, or relative (`current-#`) where `#` is days before current | +| `weekend` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | +| `weekly` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | +| `monthly` | `january`, `february`, ..., `december`, `current`, or relative (`current-#`) where `#` is days before current | +| `quarterly` | `q1`, `q2`, `q3`, `q4`, `current`, or relative (`current-#`) where `#` is days before current | +| `season` | `winter`, `spring`, `summer`, `fall`, `holiday`, or `current` | +| `holiday` | `new_years_day`, `new_year_weekend`, `mlk_day`, `mlk_day_weekend`, `presidents_day`, `presidents_day_weekend`, `easter`, `easter_weekend`, `memorial_day`, `memorial_day_weekend`, `independence_day`, `independence_day_weekend`, `labor_day`, `labor_day_weekend`, `indigenous_day`, `indigenous_day_weekend`, `halloween`, `thanksgiving`, `thanksgiving_3`, `thanksgiving_4`, `thanksgiving_5`, `post_thanksgiving_weekend`, `christmas_day`, `christmas_weekend`, `new_years_eve` | + +### Example Mojo Domestic Builder(s) + +```yaml +collections: + Current Domestic Box Office: + mojo_domestic: + range: yearly + year: current + + Last Year's Domestic Box Office: + mojo_domestic: + range: yearly + year: current-1 + + Last Month's Top 10 Domestic Box Office: + mojo_domestic: + range: monthly + range_data: current + year: current-1 + limit: 10 +``` \ No newline at end of file diff --git a/docs/files/builders/boxofficemojo/international.md b/docs/files/builders/boxofficemojo/international.md new file mode 100644 index 000000000..eeea3019f --- /dev/null +++ b/docs/files/builders/boxofficemojo/international.md @@ -0,0 +1,53 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo International + +Uses the International Box Office to collect items. + +**Builder Attribute:** `mojo_international` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `range` | Determines the type of time range of the Box Office.
**Allowed Values:** `weekend`, `monthly`, `quarterly`, or `yearly` | +| `chart` | Determines the chart you want to use.
**Default Value:** `international`
**Allowed Values:** Item in the dropdown found [here](https://www.boxofficemojo.com/intl/) | +| `year` | Determines the year of the Box Office.
**Default Value:** `current`
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | +| `range_data` | Determines the actual time range of the Box Office. Required for all ranges except `yearly`. Input depends on the `range` selected. | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +## Allowed Values for `range_data` + +| Range | Allowed Values | +|-------------|---------------------------------------------------------------------------------------------------------------| +| `weekend` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | +| `monthly` | `january`, `february`, ..., `december`, `current`, or relative (`current-#`) where `#` is days before current | +| `quarterly` | `q1`, `q2`, `q3`, `q4`, `current`, or relative (`current-#`) where `#` is days before current | + +### Example Mojo International Builder(s) + +```yaml +collections: + + Current International Box Office: + mojo_international: + range: yearly + year: current + + Last Year's International Box Office: + mojo_international: + range: yearly + year: current-1 + + Last Month's Top 10 German Box Office: + mojo_international: + range: monthly + range_data: current + chart: germany + year: current-1 + limit: 10 +``` \ No newline at end of file diff --git a/docs/files/builders/boxofficemojo/never.md b/docs/files/builders/boxofficemojo/never.md new file mode 100644 index 000000000..379ad05c8 --- /dev/null +++ b/docs/files/builders/boxofficemojo/never.md @@ -0,0 +1,41 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo Never Hit" + +Uses the [Never Hit Lists](https://www.boxofficemojo.com/charts/overall/) (Bottom Section) to collect items. + +**Builder Attribute:** `mojo_never` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `chart` | Determines the chart you want to use.
**Allowed Values:** Item in the dropdown found [here](https://www.boxofficemojo.com/charts/overall/) | +| `never` | Determines the never filter to use.
**Default Value:** `1`
**Allowed Values:** `1`, `5`, or `10` | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +### Example Mojo Never Hit Builder(s) + +```yaml +collections: + + "Top 100 Domestic Never #1": + mojo_never: + chart: domestic + limit: 100 + + "Top 100 Domestic Never #10": + mojo_never: + chart: domestic + never: 10 + limit: 100 + + "Top 100 German Never #1": + mojo_never: + chart: germany + limit: 100 +``` diff --git a/docs/files/builders/boxofficemojo/overview.md b/docs/files/builders/boxofficemojo/overview.md new file mode 100644 index 000000000..1e5655862 --- /dev/null +++ b/docs/files/builders/boxofficemojo/overview.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo Builders + +You can find items using the lists on [boxofficemojo.com](https://www.boxofficemojo.com/) (BoxOfficeMojo). + + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:---------------------------------------|:-----------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| +| [`mojo_domestic`](#domestic) | Uses the Domestic Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`mojo_international`](#international) | Uses the International Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`mojo_world`](#world) | Uses the Worldwide Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`mojo_all_time`](#all-time) | Uses the All Time lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`mojo_never`](#never) | Uses the Never Hit lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`mojo_record`](#record) | Uses other Record lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | diff --git a/docs/files/builders/boxofficemojo/record.md b/docs/files/builders/boxofficemojo/record.md new file mode 100644 index 000000000..b2298eb6a --- /dev/null +++ b/docs/files/builders/boxofficemojo/record.md @@ -0,0 +1,40 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo Other Records + +Uses the [Weekend Records](https://www.boxofficemojo.com/charts/weekend/), [Daily Records](https://www.boxofficemojo.com/charts/daily/), +and [Miscellaneous Records](https://www.boxofficemojo.com/charts/misc/) to collect items. + +**Builder Attribute:** `mojo_record` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `chart` | Determines the record you want to use.
**Allowed Values:**
`second_weekend_drop`, `post_thanksgiving_weekend_drop`, `top_opening_weekend`, `worst_opening_weekend_theater_avg`, `mlk_opening`, `easter_opening`, `memorial_opening`, `labor_opening`, `president_opening`, `thanksgiving_3_opening`, `thanksgiving_5_opening`, `mlk`, `easter`, `4th`, `memorial`, `labor`, `president`, `thanksgiving_3`, `thanksgiving_5`, `january`, `february`, `march`, `april`, `may`, `june`, `july`, `august`, `september`, `october`, `november`, `december`, `spring`, `summer`, `fall`, `holiday_season`, `winter`, `g`, `g/pg`, `pg`, `pg-13`, `r`, `nc-17`, `top_opening_weekend_theater_avg_all`, `top_opening_weekend_theater_avg_wide`, `opening_day`, `single_day_grosses`, `christmas_day_gross`, `new_years_day_gross`, `friday`, `saturday`, `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday_non_opening`, `saturday_non_opening`, `sunday_non_opening`, `monday_non_opening`, `tuesday_non_opening`, `wednesday_non_opening`, `thursday_non_opening`, `biggest_theater_drop`, `opening_week` | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +### Example Mojo Other Records Builder(s) + +```yaml +collections: + + Top 10 Biggest Opening Weekends: + mojo_record: + chart: top_opening_weekend + limit: 10 + + Top 10 Biggest Opening Day: + mojo_record: + chart: opening_day + limit: 10 + + Top 10 Biggest Opening Weeks: + mojo_record: + chart: opening_week + limit: 10 +``` \ No newline at end of file diff --git a/docs/files/builders/boxofficemojo/world.md b/docs/files/builders/boxofficemojo/world.md new file mode 100644 index 000000000..eb6290d1f --- /dev/null +++ b/docs/files/builders/boxofficemojo/world.md @@ -0,0 +1,37 @@ +--- +hide: + - toc +--- +# BoxOfficeMojo Worldwide + +Uses the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to collect items. + +**Builder Attribute:** `mojo_world` + +**Builder Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes + +## Builder Attributes + +| Attribute | Description | +|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `year` | The year of the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to pull.
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | +| `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | + +### Example Mojo Worldwide Builder(s) + +```yaml +collections: + + Current Worldwide Box Office: + mojo_world: + year: current + + Last Year's Worldwide Box Office: + mojo_world: + year: current-1 + + 2020 Top 10 Worldwide Box Office: + mojo_world: + year: 2020 + limit: 10 +``` \ No newline at end of file diff --git a/docs/files/builders/icheckmovies.md b/docs/files/builders/icheckmovies/list.md similarity index 56% rename from docs/files/builders/icheckmovies.md rename to docs/files/builders/icheckmovies/list.md index d47c31f78..833992c71 100644 --- a/docs/files/builders/icheckmovies.md +++ b/docs/files/builders/icheckmovies/list.md @@ -2,19 +2,11 @@ hide: - toc --- -# ICheckMovies Builders - -You can find items using the lists on [ICheckMovies.com](https://www.icheckmovies.com/) (ICheckMovies). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:------------------------------------------|:-------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| -| [`icheckmovies_list`](#icheckmovies-list) | Finds every movie in the ICheckMovies List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - -## ICheckMovies List +# ICheckMovies List Finds every movie in the ICheckMovies List. -The expected input is a ICheckMovies List URL. Multiple values are supported as either a list or a comma-separated string. +The expected input is a ICheckMovies List URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. diff --git a/docs/files/builders/icheckmovies/overview.md b/docs/files/builders/icheckmovies/overview.md new file mode 100644 index 000000000..eb1c66ab3 --- /dev/null +++ b/docs/files/builders/icheckmovies/overview.md @@ -0,0 +1,11 @@ +--- +hide: + - toc +--- +# ICheckMovies Builders + +You can find items using the lists on [ICheckMovies.com](https://www.icheckmovies.com/) (ICheckMovies). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------------|:-------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| +| [`icheckmovies_list`](list.md) | Finds every movie in the ICheckMovies List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | diff --git a/docs/files/builders/imdb.md b/docs/files/builders/imdb.md deleted file mode 100644 index c3cda1219..000000000 --- a/docs/files/builders/imdb.md +++ /dev/null @@ -1,561 +0,0 @@ ---- -hide: - - toc ---- -# IMDb Builders - -You can find items using the features of [IMDb.com](https://www.imdb.com/) (IMDb). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:------------------------------------|:----------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`imdb_id`](#imdb-id) | Gets the movie/show specified. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`imdb_chart`](#imdb-chart) | Gets every movie/show in an IMDb Chart like [IMDb Top 250 Movies](https://www.imdb.com/chart/top). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`imdb_search`](#imdb-search) | Gets every movie/show in an [IMDb Search](https://www.imdb.com/search/title/). Can also be used for Keyword searches. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`imdb_list`](#imdb-list) | Gets every movie/show in an IMDb List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`imdb_watchlist`](#imdb-watchlist) | Gets every movie/show in an IMDb User's Watchlist. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`imdb_award`](#imdb-award) | Gets every movie/show in an [IMDb Event](https://www.imdb.com/event/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - - -=== "IMDb ID" - - Gets the movie/show specified. - - The expected input is an IMDb ID. Multiple values are supported as either a list or a comma-separated string. - - ### Example IMDb ID Builder(s) - - ```yaml - collections: - Star Wars (Animated Shows): - imdb_id: tt0458290, tt2930604 - ``` - -=== "IMDb Chart" - - Finds every item in an IMDb Chart. - - The expected input are the options below. Multiple values are supported as either a list or a comma-separated string. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - | Name | Attribute | Works with Movies | Works with Shows | - |:-------------------------------------------------------------------------------------|:------------------|:------------------------------------------:|:------------------------------------------:| - | [Box Office](https://www.imdb.com/chart/boxoffice) | `box_office` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Most Popular Movies](https://www.imdb.com/chart/moviemeter) | `popular_movies` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top 250 Movies](https://www.imdb.com/chart/top) | `top_movies` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top Rated English Movies](https://www.imdb.com/chart/top-english-movies) | `top_english` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Most Popular TV Shows](https://www.imdb.com/chart/tvmeter) | `popular_shows` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | [Top 250 TV Shows](https://www.imdb.com/chart/toptv) | `top_shows` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | [Lowest Rated Movies](https://www.imdb.com/chart/bottom) | `lowest_rated` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top Rated Indian Movies](https://www.imdb.com/india/top-rated-indian-movies/) | `top_indian` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top Rated Tamil Movies](https://www.imdb.com/india/top-rated-tamil-movies/) | `top_tamil` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top Rated Telugu Movies](https://www.imdb.com/india/top-rated-telugu-movies/) | `top_telugu` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Top Rated Malayalam Movies](https://www.imdb.com/india/top-rated-malayalam-movies/) | `top_malayalam` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Trending Indian Movies & Shows](https://www.imdb.com/india/upcoming/) | `trending_india` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | [Trending Tamil Movies](https://www.imdb.com/india/tamil/) | `trending_tamil` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [Trending Telugu Movies](https://www.imdb.com/india/telugu/) | `trending_telugu` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - - ### Example IMDb Chart Builder(s) - - ```yaml - collections: - IMDb Top 250: - imdb_chart: top_movies - collection_order: custom - sync_mode: sync - ``` - - -=== "IMDb Search" - - Finds every item using an [IMDb Advance Title Search](https://www.imdb.com/search/title/). - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ???+ warning - - We strongly recommend you use the [IMDb Search website](https://www.imdb.com/search/) to manually verify that the options you have selected are valid. - - You can also view the available keywords on the [IMDb Keyword Search page](https://www.imdb.com/search/keyword/). - - | Search Parameter | Description | - |:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `100` | - | `sort_by` | Choose from one of the many available sort options.
**Options:** `popularity.asc`, `popularity.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `votes.asc`, `votes.desc`, `box_office.asc`, `box_office.desc`, `runtime.asc`, `runtime.desc`, `year.asc`, `year.desc`, `release.asc`, `release.desc`
**Default:** `popularity.asc` | - | `title` | Search by title name.
**Options:** Any String | - | `type` | Item must match at least one given type. Can be a comma-separated list.
**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` | - | `type.not` | Item must not match any of the given types. Can be a comma-separated list.
**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` | - | `release.after` | Item must have been released after the given date.
**Options:** `today` or Date in the format `YYYY-MM-DD` | - | `release.before` | Item must have been released before the given date.
**Options:** `today` or Date in the format `YYYY-MM-DD` | - | `rating.gte` | Item must have an IMDb Rating greater than or equal to the given number.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | - | `rating.lte` | Item must have an IMDb Rating less than or equal to the given number.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | - | `votes.gte` | Item must have a Number of Votes greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `votes.lte` | Item must have a Number of Votes less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `genre` | Item must match all genres given. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | - | `genre.any` | Item must match at least one given genre. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | - | `genre.not` | Item must not match any of the given genres. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | - | `interests` | Item must match all interests given. Can be a comma-separated list.
**Options:** see below Interests Options table | - | `interests.any` | Item must match at least one given interest. Can be a comma-separated list.
**Options:** see below Interests Options table | - | `interests.not` | Item must not match any of the given interests. Can be a comma-separated list.
**Options:** see below Interests Options table | - | `event` | Item must have been nominated for a category at the event given. Can be a comma-separated list.
**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) | - | `event.winning` | Item must have won a category at the event given. Can be a comma-separated list.
**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) | - | `imdb_top` | Item must be in the top number of given Movies.
**Options:** Any Integer greater than `0` | - | `imdb_bottom` | Item must be in the bottom number of given Movies.
**Options:** Any Integer greater than `0` | - | `topic` | Item must match at least one given topic. Can be a comma-separated list.
**Options:** `alternate_version`, `award`, `business_info`, `crazy_credit`, `goof`, `location`, `plot`, `quote`, `soundtrack`, `technical`, `trivia` | - | `topic.not` | Item must not match any of the given topic. Can be a comma-separated list.
**Options:** `alternate_version`, `award`, `business_info`, `crazy_credit`, `goof`, `location`, `plot`, `quote`, `soundtrack`, `technical`, `trivia` | - | `alternate_version` | Item's Alternate Version must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `alternate_version.any` | Item's Alternate Version must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `alternate_version.not` | Item's Alternate Version must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `crazy_credit` | Item's Crazy Credits must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `crazy_credit.any` | Item's Crazy Credits must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `crazy_credit.not` | Item's Crazy Credits must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `location` | Item's Location must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `location.any` | Item's Location must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `location.not` | Item's Location must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `goof` | Item's Goofs must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `goof.any` | Item's Goofs must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `goof.not` | Item's Goofs must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `plot` | Item's Plot must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `plot.any` | Item's Plot must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `plot.not` | Item's Plot must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `quote` | Item's Quote must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `quote.any` | Item's Quote must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `quote.not` | Item's Quote must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `soundtrack` | Item's Soundtrack must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `soundtrack.any` | Item's Soundtrack must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `soundtrack.not` | Item's Soundtrack must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `trivia` | Item's Trivia must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | - | `trivia.any` | Item's Trivia must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `trivia.not` | Item's Trivia must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | - | `company` | Item must have been released by any company given. Can be a comma-separated list.
**Options:** `fox`, `dreamworks`, `mgm`, `paramount`, `sony`, `universal`, `disney`, `warner`, or any IMDb Company ID (ex. `co0023400`) | - | `content_rating` | Item must have the given content rating. Can be a list.
**Options:** Dictionary with two attributes `rating` and `region`
`rating`: Any String to match the content rating
`region`: [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `country` | Item must match with every given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `country.any` | Item must match at least one given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `country.not` | Item must not match any given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `country.origin` | Item must match any given country as the origin country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `keyword` | Item must match with every given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | - | `keyword.any` | Item must match at least one given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | - | `keyword.not` | Item must not match any given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | - | `series` | Item must match at least one given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | - | `series.not` | Item must not match any given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | - | `list` | Item must be on every given list. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | - | `list.any` | Item must be on at least one given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | - | `list.not` | Item must not be on any given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | - | `language` | Item must match any given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | - | `language.any` | Item must match at least one given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | - | `language.not` | Item must not match any given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | - | `language.primary` | Item must match any given language as the primary language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | - | `popularity.gte` | Item must have a Popularity greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `popularity.lte` | Item must have a Popularity less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `cast` | Item must have all the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | - | `cast.any` | Item must have any of the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | - | `cast.not` | Item must not have any of the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | - | `character` | Item must have any of the given character listed in its credits. Can be a comma-separated list.
**Options:** Any String | - | `runtime.gte` | Item must have a Runtime greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `runtime.lte` | Item must have a Runtime less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | - | `adult` | Include adult titles in the search results.
**Options:** `true`/`false` | - - ??? tip "Interests Options" - - You can use the Key or the ID for interests - - | Name | Key | ID | - |:---------------------------------|:-----------------------------------|--------------| - | Action | `action` | `in0000001` | - | Action Epic | `action_epic` | `in0000002` | - | Adult Animation | `adult_animation` | `in0000025` | - | Adventure | `adventure` | `in0000012` | - | Adventure Epic | `adventure_epic` | `in0000015` | - | Alien Invasion | `alien_invasion` | `in0000157` | - | Animal Adventure | `animal_adventure` | `in0000092` | - | Animation | `animation` | `in0000026` | - | Anime | `anime` | `in0000027` | - | Artificial Intelligence | `artificial_intelligence` | `in0000158` | - | B-Action | `b_action` | `in0000003` | - | B-Horror | `b_horror` | `in0000108` | - | Baseball | `baseball` | `in0000167` | - | Basketball | `basketball` | `in0000168` | - | Beauty Competition | `beauty_competition` | `in0000102` | - | Beauty Makeover | `beauty_makeover` | `in0000123` | - | Biography | `biography` | `in0000072` | - | Body Horror | `body_horror` | `in0000109` | - | Body Swap Comedy | `body_swap_comedy` | `in0000031` | - | Boxing | `boxing` | `in0000169` | - | Buddy Comedy | `buddy_comedy` | `in0000032` | - | Buddy Cop | `buddy_cop` | `in0000033` | - | Bumbling Detective | `bumbling_detective` | `in0000136` | - | Business Reality TV | `business_reality_tv` | `in0000142` | - | Caper | `caper` | `in0000050` | - | Car Action | `car_action` | `in0000004` | - | Classic Musical | `classic_musical` | `in0000131` | - | Classical Western | `classical_western` | `in0000187` | - | Comedy | `comedy` | `in0000034` | - | Coming-of-Age | `coming_of_age` | `in0000073` | - | Computer Animation | `computer_animation` | `in0000028` | - | Concert | `concert` | `in0000129` | - | Conspiracy Thriller | `conspiracy_thriller` | `in0000176` | - | Contemporary Western | `contemporary_western` | `in0000188` | - | Cooking & Food | `cooking_&_food` | `in0000124` | - | Cooking Competition | `cooking_competition` | `in0000103` | - | Cop Drama | `cop_drama` | `in0000051` | - | Costume Drama | `costume_drama` | `in0000074` | - | Cozy Mystery | `cozy_mystery` | `in0000137` | - | Crime | `crime` | `in0000052` | - | Crime Documentary | `crime_documentary` | `in0000059` | - | Crime Reality TV | `crime_reality_tv` | `in0000143` | - | Cyber Thriller | `cyber_thriller` | `in0000177` | - | Cyberpunk | `cyberpunk` | `in0000159` | - | Dark Comedy | `dark_comedy` | `in0000035` | - | Dark Fantasy | `dark_fantasy` | `in0000095` | - | Dark Romance | `dark_romance` | `in0000149` | - | Dating Reality TV | `dating_reality_tv` | `in0000144` | - | Desert Adventure | `desert_adventure` | `in0000013` | - | Dinosaur Adventure | `dinosaur_adventure` | `in0000014` | - | Disaster | `disaster` | `in0000005` | - | Docudrama | `docudrama` | `in0000075` | - | Documentary | `documentary` | `in0000060` | - | Docuseries | `docuseries` | `in0000061` | - | Docusoap Reality TV | `docusoap_reality_tv` | `in0000145` | - | Drama | `drama` | `in0000076` | - | Drug Crime | `drug_crime` | `in0000053` | - | Dystopian Sci-Fi | `dystopian_sci_fi` | `in0000160` | - | Epic | `epic` | `in0000077` | - | Erotic Thriller | `erotic_thriller` | `in0000178` | - | Extreme Sport | `extreme_sport` | `in0000170` | - | Fairy Tale | `fairy_tale` | `in0000097` | - | Faith & Spirituality Documentary | `faith_&_spirituality_documentary` | `in0000062` | - | Family | `family` | `in0000093` | - | Fantasy | `fantasy` | `in0000098` | - | Fantasy Epic | `fantasy_epic` | `in0000096` | - | Farce | `farce` | `in0000036` | - | Feel-Good Romance | `feel_good_romance` | `in0000151` | - | Film Noir | `film_noir` | `in0000054` | - | Financial Drama | `financial_drama` | `in0000078` | - | Folk Horror | `folk_horror` | `in0000110` | - | Food Documentary | `food_documentary` | `in0000063` | - | Football | `football` | `in0000171` | - | Found Footage Horror | `found_footage_horror` | `in0000111` | - | Game Show | `game_show` | `in0000105` | - | Gangster | `gangster` | `in0000055` | - | Giallo | `giallo` | `in0000179` | - | Globetrotting Adventure | `globetrotting_adventure` | `in0000016` | - | Gun Fu | `gun_fu` | `in0000197` | - | Hand-Drawn Animation | `hand_drawn_animation` | `in0000029` | - | Hard-boiled Detective | `hard_boiled_detective` | `in0000138` | - | Heist | `heist` | `in0000056` | - | Hidden Camera | `hidden_camera` | `in0000146` | - | High-Concept Comedy | `high_concept_comedy` | `in0000037` | - | Historical Epic | `historical_epic` | `in0000079` | - | History | `history` | `in0000080` | - | History Documentary | `history_documentary` | `in0000064` | - | Holiday | `holiday` | `in0000192` | - | Holiday Animation | `holiday_animation` | `in0000193` | - | Holiday Comedy | `holiday_comedy` | `in0000194` | - | Holiday Family | `holiday_family` | `in0000195` | - | Holiday Romance | `holiday_romance` | `in0000196` | - | Home Improvement | `home_improvement` | `in0000125` | - | Horror | `horror` | `in0000112` | - | Isekai | `isekai` | `in0000201` | - | Iyashikei | `iyashikei` | `in0000202` | - | Josei | `josei` | `in0000203` | - | Jukebox Musical | `jukebox_musical` | `in0000132` | - | Jungle Adventure | `jungle_adventure` | `in0000017` | - | Kaiju | `kaiju` | `in0000161` | - | Korean Drama | `korean_drama` | `in0000209` | - | Kung Fu | `kung_fu` | `in0000198` | - | Legal Drama | `legal_drama` | `in0000081` | - | Legal Thriller | `legal_thriller` | `in0000180` | - | Lifestyle | `lifestyle` | `in0000126` | - | Martial Arts | `martial_arts` | `in0000006` | - | Mecha | `mecha` | `in0000204` | - | Medical Drama | `medical_drama` | `in0000082` | - | Military Documentary | `military_documentary` | `in0000065` | - | Mockumentary | `mockumentary` | `in0000038` | - | Monster Horror | `monster_horror` | `in0000113` | - | Motorsport | `motorsport` | `in0000172` | - | Mountain Adventure | `mountain_adventure` | `in0000018` | - | Music | `music` | `in0000130` | - | Music Documentary | `music_documentary` | `in0000066` | - | Musical | `musical` | `in0000133` | - | Mystery | `mystery` | `in0000139` | - | Nature Documentary | `nature_documentary` | `in0000067` | - | News | `news` | `in0000211` | - | One-Person Army Action | `one_person_army_action` | `in0000007` | - | Paranormal Reality TV | `paranormal_reality_tv` | `in0000147` | - | Parody | `parody` | `in0000039` | - | Period Drama | `period_drama` | `in0000083` | - | Police Procedural | `police_procedural` | `in0000057` | - | Political Documentary | `political_documentary` | `in0000068` | - | Political Drama | `political_drama` | `in0000084` | - | Political Thriller | `political_thriller` | `in0000181` | - | Pop Musical | `pop_musical` | `in0000134` | - | Prison Drama | `prison_drama` | `in0000085` | - | Psychological Drama | `psychological_drama` | `in0000086` | - | Psychological Horror | `psychological_horror` | `in0000114` | - | Psychological Thriller | `psychological_thriller` | `in0000182` | - | Quest | `quest` | `in0000019` | - | Quirky Comedy | `quirky_comedy` | `in0000040` | - | Quiz Show | `quiz_show` | `in0000104` | - | Raunchy Comedy | `raunchy_comedy` | `in0000041` | - | Reality TV | `reality_tv` | `in0000148` | - | Road Trip | `road_trip` | `in0000020` | - | Rock Musical | `rock_musical` | `in0000135` | - | Romance | `romance` | `in0000152` | - | Romantic Comedy | `romantic_comedy` | `in0000153` | - | Romantic Epic | `romantic_epic` | `in0000150` | - | Samurai | `samurai` | `in0000199` | - | Satire | `satire` | `in0000042` | - | Sci-Fi | `sci_fi` | `in0000162` | - | Sci-Fi Epic | `sci_fi_epic` | `in0000163` | - | Science & Technology Documentary | `science_&_technology_documentary` | `in0000069` | - | Screwball Comedy | `screwball_comedy` | `in0000043` | - | Sea Adventure | `sea_adventure` | `in0000021` | - | Seinen | `seinen` | `in0000205` | - | Serial Killer | `serial_killer` | `in0000183` | - | Short | `short` | `in0000212` | - | Showbiz Drama | `showbiz_drama` | `in0000087` | - | Shōjo | `shōjo` | `in0000207` | - | Shōnen | `shōnen` | `in0000206` | - | Sitcom | `sitcom` | `in0000044` | - | Sketch Comedy | `sketch_comedy` | `in0000045` | - | Slapstick | `slapstick` | `in0000046` | - | Slasher Horror | `slasher_horror` | `in0000115` | - | Slice of Life | `slice_of_life` | `in0000208` | - | Soap Opera | `soap_opera` | `in0000088` | - | Soccer | `soccer` | `in0000173` | - | Space Sci-Fi | `space_sci_fi` | `in0000164` | - | Spaghetti Western | `spaghetti_western` | `in0000190` | - | Splatter Horror | `splatter_horror` | `in0000116` | - | Sport | `sport` | `in0000174` | - | Sports Documentary | `sports_documentary` | `in0000070` | - | Spy | `spy` | `in0000184` | - | Stand-Up | `stand_up` | `in0000047` | - | Steampunk | `steampunk` | `in0000165` | - | Steamy Romance | `steamy_romance` | `in0000154` | - | Stoner Comedy | `stoner_comedy` | `in0000048` | - | Stop Motion Animation | `stop_motion_animation` | `in0000030` | - | Superhero | `superhero` | `in0000008` | - | Supernatural Fantasy | `supernatural_fantasy` | `in0000099` | - | Supernatural Horror | `supernatural_horror` | `in0000117` | - | Survival | `survival` | `in0000185` | - | Survival Competition | `survival_competition` | `in0000106` | - | Suspense Mystery | `suspense_mystery` | `in0000140` | - | Swashbuckler | `swashbuckler` | `in0000022` | - | Sword & Sandal | `sword_&_sandal` | `in0000009` | - | Sword & Sorcery | `sword_&_sorcery` | `in0000100` | - | Talent Competition | `talent_competition` | `in0000107` | - | Talk Show | `talk_show` | `in0000127` | - | Teen Adventure | `teen_adventure` | `in0000023` | - | Teen Comedy | `teen_comedy` | `in0000049` | - | Teen Drama | `teen_drama` | `in0000089` | - | Teen Fantasy | `teen_fantasy` | `in0000101` | - | Teen Horror | `teen_horror` | `in0000118` | - | Teen Romance | `teen_romance` | `in0000155` | - | Telenovela | `telenovela` | `in0000210` | - | Thriller | `thriller` | `in0000186` | - | Time Travel | `time_travel` | `in0000166` | - | Tragedy | `tragedy` | `in0000090` | - | Tragic Romance | `tragic_romance` | `in0000156` | - | Travel | `travel` | `in0000128` | - | Travel Documentary | `travel_documentary` | `in0000071` | - | True Crime | `true_crime` | `in0000058` | - | Urban Adventure | `urban_adventure` | `in0000024` | - | Vampire Horror | `vampire_horror` | `in0000119` | - | War | `war` | `in0000010` | - | War Epic | `war_epic` | `in0000011` | - | Water Sport | `water_sport` | `in0000175` | - | Werewolf Horror | `werewolf_horror` | `in0000120` | - | Western | `western` | `in0000191` | - | Western Epic | `western_epic` | `in0000189` | - | Whodunnit | `whodunnit` | `in0000141` | - | Witch Horror | `witch_horror` | `in0000121` | - | Workplace Drama | `workplace_drama` | `in0000091` | - | Wuxia | `wuxia` | `in0000200` | - | Zombie Horror | `zombie_horror` | `in0000122` | - - - ### Example IMDb Search Builder(s) - - ```yaml - collections: - IMDb Popular: - imdb_search: - type: movie - sort_by: popularity.asc - limit: 50 - collection_order: custom - sync_mode: sync - ``` - - ```yaml - collections: - Top Action: - imdb_search: - type: movie - release.after: 1990-01-01 - rating.gte: 5 - votes.gte: 100000 - genre: action - sort_by: rating.desc - limit: 100 - ``` - - You can also find episodes using `imdb_search` like so. - - ```yaml - collections: - The Simpsons Top 100 Episodes: - collection_order: custom - builder_level: episode - sync_mode: sync - imdb_search: - type: tv_episode - series: tt0096697 - sort_by: rating.desc - limit: 100 - summary: The top 100 Simpsons episodes by IMDb user rating - ``` - -=== "IMDb List" - - ???+ danger "Important Notice" - - Due to recent changes in IMDb's code, `imdb_list` can no longer be used for any url which starts with - `https://www.imdb.com/search/` or `https://www.imdb.com/filmosearch/`. - - These must instead use the [IMDb Search Builder](#imdb-search) - - - Finds every item in an IMDb List. - - | List Parameter | Description | - |:---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `list_id` | Specify the IMDb List ID. **This attribute is required.**
**Options:** The ID that starts with `ls` found in the URL of the list. (ex. `ls005526372`) | - | `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `0` | - | `sort_by` | Choose from one of the many available sort options.
**Options:** `custom.asc`, `custom.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `popularity.asc`, `popularity.desc`, `votes.asc`, `votes.desc`, `release.asc`, `release.desc`, `runtime.asc`, `runtime.desc`, `added.asc`, `added.desc`
**Default:** `custom.asc` | - - Multiple values are supported as a list only a comma-separated string will not work. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example IMDb List Builder(s) - - ```yaml - collections: - James Bonds: - imdb_list: - list_id: ls006405458 - limit: 100 - sort_by: rating.asc - collection_order: custom - sync_mode: sync - ``` - - You can search multiple lists in one collection by using a list. - - ```yaml - collections: - Christmas: - imdb_list: - - list_id: ls025976544 - limit: 10 - sort_by: rating.asc - - list_id: ls003863000 - limit: 10 - sort_by: rating.asc - - list_id: ls027454200 - limit: 10 - sort_by: rating.asc - - list_id: ls027886673 - limit: 10 - sort_by: rating.asc - - list_id: ls097998599 - limit: 10 - sort_by: rating.asc - sync_mode: sync - collection_order: alpha - ``` - -=== "IMDb Watchlist" - - Finds every item in an IMDb User's Watchlist. - - | List Parameter | Description | - |:---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `user_id` | Specify the User ID for the IMDb Watchlist. **This attribute is required.**
**Options:** The ID that starts with `ur` found in the URL of the watchlist. (ex. `ur12345678`) | - | `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `0` | - | `sort_by` | Choose from one of the many available sort options.
**Options:** `custom.asc`, `custom.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `popularity.asc`, `popularity.desc`, `votes.asc`, `votes.desc`, `release.asc`, `release.desc`, `runtime.asc`, `runtime.desc`, `added.asc`, `added.desc`
**Default:** `custom.asc` | - - Multiple values are supported as a list only a comma-separated string will not work. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example IMDb Watchlist Builder(s) - - ```yaml - collections: - My Watch Watchlist: - imdb_watchlist: - user_id: ur64054558 - sort_by: rating.asc - collection_order: custom - sync_mode: sync - ``` - - ```yaml - collections: - My Friends Watchlists: - imdb_watchlist: - - user_id: ur64054558 - sort_by: rating.asc - limit: 100 - - user_id: ur12345678 - sort_by: rating.asc - limit: 100 - sync_mode: sync - ``` - -=== "IMDb Award" - - Finds every item in an [IMDb Event](https://www.imdb.com/event/). - - | Award Parameter | Description | - |:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `event_id` | Specify the IMDb Event ID to search. **This attribute is required.**
**Options:** The ID found in the URLs linked on the [IMDb Events Page](https://www.imdb.com/event/). (ex. `ev0000003`) | - | `event_year`**1** | Specify the year of the Event to look at. **This attribute is required.**
**Options:** `all`, any year, any negative year (ex. `-10` for 10 years ago), list of years, or year range (ex. `2000-2009`, `2000-latest`, or `-30-latest` (Last 30 Years)) from the years under the Event History Sidebar on an Event page. | - | `award_filter` | Filter by the Award heading. Can only accept multiple values as a list.
**Options:** Any Black Award heading on an Event Page. | - | `category_filter` | Filter by the Category heading. Can only accept multiple values as a list.
**Options:** Any Gold/Yellow Category heading on an Event Page. | - | `winning` | Filter by if the Item Won the award.
**Options:** `true`/`false`
**Default:** `false` | - - ???+ example "Example Award and Category Filter" - - In the below example, "Grand Jury Prize" is the award_filter, and "Documentary" is the `category_filter`. You can use both of these filters together. - - ![imdbfilter.png](../../assets/images/files/builders/imdb-award-filters.png) - - 1. When using multiple years the only available Event IDs are: - - ```yaml - {% - include-markdown "https://raw.githubusercontent.com/Kometa-Team/IMDb-Awards/master/event_ids.yml" - comments=false - %} - ``` - ### Example IMDb Award Builder(s) - - ```yaml - collections: - Academy Award Winners 2023: - imdb_award: - event_id: ev0000003 - event_year: 2023 - winning: true - ``` - ```yaml - collections: - Academy Award 2023 Best Picture Nominees: - imdb_award: - event_id: ev0000003 - event_year: 2023 - category_filter: Best Motion Picture of the Year - ``` diff --git a/docs/files/builders/imdb/award.md b/docs/files/builders/imdb/award.md new file mode 100644 index 000000000..ea9f334c5 --- /dev/null +++ b/docs/files/builders/imdb/award.md @@ -0,0 +1,48 @@ +--- +hide: + - toc +--- +# IMDb Award + +Finds every item in an [IMDb Event](https://www.imdb.com/event/). + +| Award Parameter | Description | +|:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `event_id` | Specify the IMDb Event ID to search. **This attribute is required.**
**Options:** The ID found in the URLs linked on the [IMDb Events Page](https://www.imdb.com/event/). (ex. `ev0000003`) | +| `event_year`**1** | Specify the year of the Event to look at. **This attribute is required.**
**Options:** `all`, any year, any negative year (ex. `-10` for 10 years ago), list of years, or year range (ex. `2000-2009`, `2000-current`, or `-30-current` (Last 30 Years)) from the years under the Event History Sidebar on an Event page. | +| `award_filter` | Filter by the Award heading. Can only accept multiple values as a list.
**Options:** Any Black Award heading on an Event Page. | +| `category_filter` | Filter by the Category heading. Can only accept multiple values as a list.
**Options:** Any Gold/Yellow Category heading on an Event Page. | +| `winning` | Filter by if the Item Won the award.
**Options:** `true`/`false`
**Default:** `false` | + +???+ example "Example Award and Category Filter" + + In the below example, "Grand Jury Prize" is the award_filter, and "Documentary" is the `category_filter`. You can use both of these filters together. + + ![imdbfilter.png](../../assets/images/files/builders/imdb-award-filters.png) + +1. When using multiple years the only available Event IDs are: + +```yaml +{% + include-markdown "https://raw.githubusercontent.com/Kometa-Team/IMDb-Awards/master/event_ids.yml" + comments=false +%} +``` +### Example IMDb Award Builder(s) + +```yaml +collections: + Academy Award Winners 2023: + imdb_award: + event_id: ev0000003 + event_year: 2023 + winning: true +``` +```yaml +collections: + Academy Award 2023 Best Picture Nominees: + imdb_award: + event_id: ev0000003 + event_year: 2023 + category_filter: Best Motion Picture of the Year +``` diff --git a/docs/files/builders/imdb/chart.md b/docs/files/builders/imdb/chart.md new file mode 100644 index 000000000..58d519f3b --- /dev/null +++ b/docs/files/builders/imdb/chart.md @@ -0,0 +1,38 @@ +--- +hide: + - toc +--- +# IMDb Chart + +Finds every item in an IMDb Chart. + +The expected input are the options below. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +| Name | Attribute | Works with Movies | Works with Shows | +|:-------------------------------------------------------------------------------------|:------------------|:------------------------------------------:|:------------------------------------------:| +| [Box Office](https://www.imdb.com/chart/boxoffice) | `box_office` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Most Popular Movies](https://www.imdb.com/chart/moviemeter) | `popular_movies` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top 250 Movies](https://www.imdb.com/chart/top) | `top_movies` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top Rated English Movies](https://www.imdb.com/chart/top-english-movies) | `top_english` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Most Popular TV Shows](https://www.imdb.com/chart/tvmeter) | `popular_shows` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [Top 250 TV Shows](https://www.imdb.com/chart/toptv) | `top_shows` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [Lowest Rated Movies](https://www.imdb.com/chart/bottom) | `lowest_rated` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top Rated Indian Movies](https://www.imdb.com/india/top-rated-indian-movies/) | `top_indian` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top Rated Tamil Movies](https://www.imdb.com/india/top-rated-tamil-movies/) | `top_tamil` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top Rated Telugu Movies](https://www.imdb.com/india/top-rated-telugu-movies/) | `top_telugu` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Top Rated Malayalam Movies](https://www.imdb.com/india/top-rated-malayalam-movies/) | `top_malayalam` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Trending Indian Movies & Shows](https://www.imdb.com/india/upcoming/) | `trending_india` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [Trending Tamil Movies](https://www.imdb.com/india/tamil/) | `trending_tamil` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [Trending Telugu Movies](https://www.imdb.com/india/telugu/) | `trending_telugu` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + +### Example IMDb Chart Builder(s) + +```yaml +collections: + IMDb Top 250: + imdb_chart: top_movies + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/imdb/id.md b/docs/files/builders/imdb/id.md new file mode 100644 index 000000000..d6ab72fd8 --- /dev/null +++ b/docs/files/builders/imdb/id.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# IMDb ID + +Gets the movie/show specified. + +The expected input is an IMDb ID. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example IMDb ID Builder(s) + +```yaml +collections: + Star Wars (Animated Shows): + imdb_id: tt0458290, tt2930604 +``` diff --git a/docs/files/builders/imdb/list.md b/docs/files/builders/imdb/list.md new file mode 100644 index 000000000..6fd6395f9 --- /dev/null +++ b/docs/files/builders/imdb/list.md @@ -0,0 +1,63 @@ +--- +hide: + - toc +--- +# IMDb List + +???+ danger "Important Notice" + + Due to recent changes in IMDb's code, `imdb_list` can no longer be used for any url which starts with + `https://www.imdb.com/search/` or `https://www.imdb.com/filmosearch/`. + + These must instead use the [IMDb Search Builder](#imdb-search) + + +Finds every item in an IMDb List. + +| List Parameter | Description | +|:---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `list_id` | Specify the IMDb List ID. **This attribute is required.**
**Options:** The ID that starts with `ls` found in the URL of the list. (ex. `ls005526372`) | +| `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `0` | +| `sort_by` | Choose from one of the many available sort options.
**Options:** `custom.asc`, `custom.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `popularity.asc`, `popularity.desc`, `votes.asc`, `votes.desc`, `release.asc`, `release.desc`, `runtime.asc`, `runtime.desc`, `added.asc`, `added.desc`
**Default:** `custom.asc` | + +Multiple values are supported as a list only a comma-separated string will not work. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example IMDb List Builder(s) + +```yaml +collections: + James Bonds: + imdb_list: + list_id: ls006405458 + limit: 100 + sort_by: rating.asc + collection_order: custom + sync_mode: sync +``` + +You can search multiple lists in one collection by using a list. + +```yaml +collections: + Christmas: + imdb_list: + - list_id: ls025976544 + limit: 10 + sort_by: rating.asc + - list_id: ls003863000 + limit: 10 + sort_by: rating.asc + - list_id: ls027454200 + limit: 10 + sort_by: rating.asc + - list_id: ls027886673 + limit: 10 + sort_by: rating.asc + - list_id: ls097998599 + limit: 10 + sort_by: rating.asc + sync_mode: sync + collection_order: alpha +``` \ No newline at end of file diff --git a/docs/files/builders/imdb/overview.md b/docs/files/builders/imdb/overview.md new file mode 100644 index 000000000..f6bc5481b --- /dev/null +++ b/docs/files/builders/imdb/overview.md @@ -0,0 +1,16 @@ +--- +hide: + - toc +--- +# IMDb Builders + +You can find items using the features of [IMDb.com](https://www.imdb.com/) (IMDb). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:------------------------------|:----------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`imdb_id`](id.md) | Gets the movie/show specified. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`imdb_chart`](chart.md) | Gets every movie/show in an IMDb Chart like [IMDb Top 250 Movies](https://www.imdb.com/chart/top). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`imdb_search`](search.md) | Gets every movie/show in an [IMDb Search](https://www.imdb.com/search/title/). Can also be used for Keyword searches. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`imdb_list`](list.md) | Gets every movie/show in an IMDb List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`imdb_watchlist`](watchlist.md) | Gets every movie/show in an IMDb User's Watchlist. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`imdb_award`](award.md) | Gets every movie/show in an [IMDb Event](https://www.imdb.com/event/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/files/builders/imdb/search.md b/docs/files/builders/imdb/search.md new file mode 100644 index 000000000..b6a9ae5a1 --- /dev/null +++ b/docs/files/builders/imdb/search.md @@ -0,0 +1,353 @@ +--- +hide: + - toc +--- +# IMDb Search + +Finds every item using an [IMDb Advance Title Search](https://www.imdb.com/search/title/). + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning + + We strongly recommend you use the [IMDb Search website](https://www.imdb.com/search/) to manually verify that the options you have selected are valid. + + You can also view the available keywords on the [IMDb Keyword Search page](https://www.imdb.com/search/keyword/). + +| Search Parameter | Description | +|:------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `100` | +| `sort_by` | Choose from one of the many available sort options.
**Options:** `popularity.asc`, `popularity.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `votes.asc`, `votes.desc`, `box_office.asc`, `box_office.desc`, `runtime.asc`, `runtime.desc`, `year.asc`, `year.desc`, `release.asc`, `release.desc`
**Default:** `popularity.asc` | +| `title` | Search by title name.
**Options:** Any String | +| `type` | Item must match at least one given type. Can be a comma-separated list.
**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` | +| `type.not` | Item must not match any of the given types. Can be a comma-separated list.
**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` | +| `release.after` | Item must have been released after the given date.
**Options:** `today` or Date in the format `YYYY-MM-DD` | +| `release.before` | Item must have been released before the given date.
**Options:** `today` or Date in the format `YYYY-MM-DD` | +| `rating.gte` | Item must have an IMDb Rating greater than or equal to the given number.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | +| `rating.lte` | Item must have an IMDb Rating less than or equal to the given number.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | +| `votes.gte` | Item must have a Number of Votes greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `votes.lte` | Item must have a Number of Votes less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `genre` | Item must match all genres given. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | +| `genre.any` | Item must match at least one given genre. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | +| `genre.not` | Item must not match any of the given genres. Can be a comma-separated list.
**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` | +| `interests` | Item must match all interests given. Can be a comma-separated list.
**Options:** see below Interests Options table | +| `interests.any` | Item must match at least one given interest. Can be a comma-separated list.
**Options:** see below Interests Options table | +| `interests.not` | Item must not match any of the given interests. Can be a comma-separated list.
**Options:** see below Interests Options table | +| `event` | Item must have been nominated for a category at the event given. Can be a comma-separated list.
**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) | +| `event.winning` | Item must have won a category at the event given. Can be a comma-separated list.
**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) | +| `imdb_top` | Item must be in the top number of given Movies.
**Options:** Any Integer greater than `0` | +| `imdb_bottom` | Item must be in the bottom number of given Movies.
**Options:** Any Integer greater than `0` | +| `topic` | Item must match at least one given topic. Can be a comma-separated list.
**Options:** `alternate_version`, `award`, `business_info`, `crazy_credit`, `goof`, `location`, `plot`, `quote`, `soundtrack`, `technical`, `trivia` | +| `topic.not` | Item must not match any of the given topic. Can be a comma-separated list.
**Options:** `alternate_version`, `award`, `business_info`, `crazy_credit`, `goof`, `location`, `plot`, `quote`, `soundtrack`, `technical`, `trivia` | +| `alternate_version` | Item's Alternate Version must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `alternate_version.any` | Item's Alternate Version must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `alternate_version.not` | Item's Alternate Version must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `crazy_credit` | Item's Crazy Credits must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `crazy_credit.any` | Item's Crazy Credits must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `crazy_credit.not` | Item's Crazy Credits must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `location` | Item's Location must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `location.any` | Item's Location must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `location.not` | Item's Location must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `goof` | Item's Goofs must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `goof.any` | Item's Goofs must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `goof.not` | Item's Goofs must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `plot` | Item's Plot must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `plot.any` | Item's Plot must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `plot.not` | Item's Plot must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `quote` | Item's Quote must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `quote.any` | Item's Quote must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `quote.not` | Item's Quote must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `soundtrack` | Item's Soundtrack must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `soundtrack.any` | Item's Soundtrack must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `soundtrack.not` | Item's Soundtrack must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `trivia` | Item's Trivia must contain all the given strings. Can be a comma-separated list.
**Options:** Any String | +| `trivia.any` | Item's Trivia must contain at least one of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `trivia.not` | Item's Trivia must not contain any of the given strings. Can be a comma-separated list.
**Options:** Any String | +| `company` | Item must have been released by any company given. Can be a comma-separated list.
**Options:** `fox`, `dreamworks`, `mgm`, `paramount`, `sony`, `universal`, `disney`, `warner`, or any IMDb Company ID (ex. `co0023400`) | +| `content_rating` | Item must have the given content rating. Can be a list.
**Options:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } with two attributes `rating` and `region`
`rating`: Any String to match the content rating
`region`: [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `country` | Item must match with every given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `country.any` | Item must match at least one given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `country.not` | Item must not match any given country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `country.origin` | Item must match any given country as the origin country. Can be a comma-separated list.
**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `keyword` | Item must match with every given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | +| `keyword.any` | Item must match at least one given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | +| `keyword.not` | Item must not match any given keyword. Can be a comma-separated list.
**Options:** Any Valid [IMDb Keyword](https://www.imdb.com/search/keyword/) | +| `series` | Item must match at least one given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | +| `series.not` | Item must not match any given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | +| `list` | Item must be on every given list. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `list.any` | Item must be on at least one given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `list.not` | Item must not be on any given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `language` | Item must match any given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | +| `language.any` | Item must match at least one given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | +| `language.not` | Item must not match any given language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | +| `language.primary` | Item must match any given language as the primary language. Can be a comma-separated list.
**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | +| `popularity.gte` | Item must have a Popularity greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `popularity.lte` | Item must have a Popularity less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `cast` | Item must have all the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | +| `cast.any` | Item must have any of the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | +| `cast.not` | Item must not have any of the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | +| `character` | Item must have any of the given character listed in its credits. Can be a comma-separated list.
**Options:** Any String | +| `runtime.gte` | Item must have a Runtime greater than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `runtime.lte` | Item must have a Runtime less than or equal to the given number.
**Options:** Any Integer greater than `0`
**Example:** `1000` | +| `adult` | Include adult titles in the search results.
**Options:** `true`/`false` | + +??? tip "Interests Options" + + You can use the Key or the ID for interests + + | Name | Key | ID | + |:---------------------------------|:-----------------------------------|--------------| + | Action | `action` | `in0000001` | + | Action Epic | `action_epic` | `in0000002` | + | Adult Animation | `adult_animation` | `in0000025` | + | Adventure | `adventure` | `in0000012` | + | Adventure Epic | `adventure_epic` | `in0000015` | + | Alien Invasion | `alien_invasion` | `in0000157` | + | Animal Adventure | `animal_adventure` | `in0000092` | + | Animation | `animation` | `in0000026` | + | Anime | `anime` | `in0000027` | + | Artificial Intelligence | `artificial_intelligence` | `in0000158` | + | B-Action | `b_action` | `in0000003` | + | B-Horror | `b_horror` | `in0000108` | + | Baseball | `baseball` | `in0000167` | + | Basketball | `basketball` | `in0000168` | + | Beauty Competition | `beauty_competition` | `in0000102` | + | Beauty Makeover | `beauty_makeover` | `in0000123` | + | Biography | `biography` | `in0000072` | + | Body Horror | `body_horror` | `in0000109` | + | Body Swap Comedy | `body_swap_comedy` | `in0000031` | + | Boxing | `boxing` | `in0000169` | + | Buddy Comedy | `buddy_comedy` | `in0000032` | + | Buddy Cop | `buddy_cop` | `in0000033` | + | Bumbling Detective | `bumbling_detective` | `in0000136` | + | Business Reality TV | `business_reality_tv` | `in0000142` | + | Caper | `caper` | `in0000050` | + | Car Action | `car_action` | `in0000004` | + | Classic Musical | `classic_musical` | `in0000131` | + | Classical Western | `classical_western` | `in0000187` | + | Comedy | `comedy` | `in0000034` | + | Coming-of-Age | `coming_of_age` | `in0000073` | + | Computer Animation | `computer_animation` | `in0000028` | + | Concert | `concert` | `in0000129` | + | Conspiracy Thriller | `conspiracy_thriller` | `in0000176` | + | Contemporary Western | `contemporary_western` | `in0000188` | + | Cooking & Food | `cooking_&_food` | `in0000124` | + | Cooking Competition | `cooking_competition` | `in0000103` | + | Cop Drama | `cop_drama` | `in0000051` | + | Costume Drama | `costume_drama` | `in0000074` | + | Cozy Mystery | `cozy_mystery` | `in0000137` | + | Crime | `crime` | `in0000052` | + | Crime Documentary | `crime_documentary` | `in0000059` | + | Crime Reality TV | `crime_reality_tv` | `in0000143` | + | Cyber Thriller | `cyber_thriller` | `in0000177` | + | Cyberpunk | `cyberpunk` | `in0000159` | + | Dark Comedy | `dark_comedy` | `in0000035` | + | Dark Fantasy | `dark_fantasy` | `in0000095` | + | Dark Romance | `dark_romance` | `in0000149` | + | Dating Reality TV | `dating_reality_tv` | `in0000144` | + | Desert Adventure | `desert_adventure` | `in0000013` | + | Dinosaur Adventure | `dinosaur_adventure` | `in0000014` | + | Disaster | `disaster` | `in0000005` | + | Docudrama | `docudrama` | `in0000075` | + | Documentary | `documentary` | `in0000060` | + | Docuseries | `docuseries` | `in0000061` | + | Docusoap Reality TV | `docusoap_reality_tv` | `in0000145` | + | Drama | `drama` | `in0000076` | + | Drug Crime | `drug_crime` | `in0000053` | + | Dystopian Sci-Fi | `dystopian_sci_fi` | `in0000160` | + | Epic | `epic` | `in0000077` | + | Erotic Thriller | `erotic_thriller` | `in0000178` | + | Extreme Sport | `extreme_sport` | `in0000170` | + | Fairy Tale | `fairy_tale` | `in0000097` | + | Faith & Spirituality Documentary | `faith_&_spirituality_documentary` | `in0000062` | + | Family | `family` | `in0000093` | + | Fantasy | `fantasy` | `in0000098` | + | Fantasy Epic | `fantasy_epic` | `in0000096` | + | Farce | `farce` | `in0000036` | + | Feel-Good Romance | `feel_good_romance` | `in0000151` | + | Film Noir | `film_noir` | `in0000054` | + | Financial Drama | `financial_drama` | `in0000078` | + | Folk Horror | `folk_horror` | `in0000110` | + | Food Documentary | `food_documentary` | `in0000063` | + | Football | `football` | `in0000171` | + | Found Footage Horror | `found_footage_horror` | `in0000111` | + | Game Show | `game_show` | `in0000105` | + | Gangster | `gangster` | `in0000055` | + | Giallo | `giallo` | `in0000179` | + | Globetrotting Adventure | `globetrotting_adventure` | `in0000016` | + | Gun Fu | `gun_fu` | `in0000197` | + | Hand-Drawn Animation | `hand_drawn_animation` | `in0000029` | + | Hard-boiled Detective | `hard_boiled_detective` | `in0000138` | + | Heist | `heist` | `in0000056` | + | Hidden Camera | `hidden_camera` | `in0000146` | + | High-Concept Comedy | `high_concept_comedy` | `in0000037` | + | Historical Epic | `historical_epic` | `in0000079` | + | History | `history` | `in0000080` | + | History Documentary | `history_documentary` | `in0000064` | + | Holiday | `holiday` | `in0000192` | + | Holiday Animation | `holiday_animation` | `in0000193` | + | Holiday Comedy | `holiday_comedy` | `in0000194` | + | Holiday Family | `holiday_family` | `in0000195` | + | Holiday Romance | `holiday_romance` | `in0000196` | + | Home Improvement | `home_improvement` | `in0000125` | + | Horror | `horror` | `in0000112` | + | Isekai | `isekai` | `in0000201` | + | Iyashikei | `iyashikei` | `in0000202` | + | Josei | `josei` | `in0000203` | + | Jukebox Musical | `jukebox_musical` | `in0000132` | + | Jungle Adventure | `jungle_adventure` | `in0000017` | + | Kaiju | `kaiju` | `in0000161` | + | Korean Drama | `korean_drama` | `in0000209` | + | Kung Fu | `kung_fu` | `in0000198` | + | Legal Drama | `legal_drama` | `in0000081` | + | Legal Thriller | `legal_thriller` | `in0000180` | + | Lifestyle | `lifestyle` | `in0000126` | + | Martial Arts | `martial_arts` | `in0000006` | + | Mecha | `mecha` | `in0000204` | + | Medical Drama | `medical_drama` | `in0000082` | + | Military Documentary | `military_documentary` | `in0000065` | + | Mockumentary | `mockumentary` | `in0000038` | + | Monster Horror | `monster_horror` | `in0000113` | + | Motorsport | `motorsport` | `in0000172` | + | Mountain Adventure | `mountain_adventure` | `in0000018` | + | Music | `music` | `in0000130` | + | Music Documentary | `music_documentary` | `in0000066` | + | Musical | `musical` | `in0000133` | + | Mystery | `mystery` | `in0000139` | + | Nature Documentary | `nature_documentary` | `in0000067` | + | News | `news` | `in0000211` | + | One-Person Army Action | `one_person_army_action` | `in0000007` | + | Paranormal Reality TV | `paranormal_reality_tv` | `in0000147` | + | Parody | `parody` | `in0000039` | + | Period Drama | `period_drama` | `in0000083` | + | Police Procedural | `police_procedural` | `in0000057` | + | Political Documentary | `political_documentary` | `in0000068` | + | Political Drama | `political_drama` | `in0000084` | + | Political Thriller | `political_thriller` | `in0000181` | + | Pop Musical | `pop_musical` | `in0000134` | + | Prison Drama | `prison_drama` | `in0000085` | + | Psychological Drama | `psychological_drama` | `in0000086` | + | Psychological Horror | `psychological_horror` | `in0000114` | + | Psychological Thriller | `psychological_thriller` | `in0000182` | + | Quest | `quest` | `in0000019` | + | Quirky Comedy | `quirky_comedy` | `in0000040` | + | Quiz Show | `quiz_show` | `in0000104` | + | Raunchy Comedy | `raunchy_comedy` | `in0000041` | + | Reality TV | `reality_tv` | `in0000148` | + | Road Trip | `road_trip` | `in0000020` | + | Rock Musical | `rock_musical` | `in0000135` | + | Romance | `romance` | `in0000152` | + | Romantic Comedy | `romantic_comedy` | `in0000153` | + | Romantic Epic | `romantic_epic` | `in0000150` | + | Samurai | `samurai` | `in0000199` | + | Satire | `satire` | `in0000042` | + | Sci-Fi | `sci_fi` | `in0000162` | + | Sci-Fi Epic | `sci_fi_epic` | `in0000163` | + | Science & Technology Documentary | `science_&_technology_documentary` | `in0000069` | + | Screwball Comedy | `screwball_comedy` | `in0000043` | + | Sea Adventure | `sea_adventure` | `in0000021` | + | Seinen | `seinen` | `in0000205` | + | Serial Killer | `serial_killer` | `in0000183` | + | Short | `short` | `in0000212` | + | Showbiz Drama | `showbiz_drama` | `in0000087` | + | Shōjo | `shōjo` | `in0000207` | + | Shōnen | `shōnen` | `in0000206` | + | Sitcom | `sitcom` | `in0000044` | + | Sketch Comedy | `sketch_comedy` | `in0000045` | + | Slapstick | `slapstick` | `in0000046` | + | Slasher Horror | `slasher_horror` | `in0000115` | + | Slice of Life | `slice_of_life` | `in0000208` | + | Soap Opera | `soap_opera` | `in0000088` | + | Soccer | `soccer` | `in0000173` | + | Space Sci-Fi | `space_sci_fi` | `in0000164` | + | Spaghetti Western | `spaghetti_western` | `in0000190` | + | Splatter Horror | `splatter_horror` | `in0000116` | + | Sport | `sport` | `in0000174` | + | Sports Documentary | `sports_documentary` | `in0000070` | + | Spy | `spy` | `in0000184` | + | Stand-Up | `stand_up` | `in0000047` | + | Steampunk | `steampunk` | `in0000165` | + | Steamy Romance | `steamy_romance` | `in0000154` | + | Stoner Comedy | `stoner_comedy` | `in0000048` | + | Stop Motion Animation | `stop_motion_animation` | `in0000030` | + | Superhero | `superhero` | `in0000008` | + | Supernatural Fantasy | `supernatural_fantasy` | `in0000099` | + | Supernatural Horror | `supernatural_horror` | `in0000117` | + | Survival | `survival` | `in0000185` | + | Survival Competition | `survival_competition` | `in0000106` | + | Suspense Mystery | `suspense_mystery` | `in0000140` | + | Swashbuckler | `swashbuckler` | `in0000022` | + | Sword & Sandal | `sword_&_sandal` | `in0000009` | + | Sword & Sorcery | `sword_&_sorcery` | `in0000100` | + | Talent Competition | `talent_competition` | `in0000107` | + | Talk Show | `talk_show` | `in0000127` | + | Teen Adventure | `teen_adventure` | `in0000023` | + | Teen Comedy | `teen_comedy` | `in0000049` | + | Teen Drama | `teen_drama` | `in0000089` | + | Teen Fantasy | `teen_fantasy` | `in0000101` | + | Teen Horror | `teen_horror` | `in0000118` | + | Teen Romance | `teen_romance` | `in0000155` | + | Telenovela | `telenovela` | `in0000210` | + | Thriller | `thriller` | `in0000186` | + | Time Travel | `time_travel` | `in0000166` | + | Tragedy | `tragedy` | `in0000090` | + | Tragic Romance | `tragic_romance` | `in0000156` | + | Travel | `travel` | `in0000128` | + | Travel Documentary | `travel_documentary` | `in0000071` | + | True Crime | `true_crime` | `in0000058` | + | Urban Adventure | `urban_adventure` | `in0000024` | + | Vampire Horror | `vampire_horror` | `in0000119` | + | War | `war` | `in0000010` | + | War Epic | `war_epic` | `in0000011` | + | Water Sport | `water_sport` | `in0000175` | + | Werewolf Horror | `werewolf_horror` | `in0000120` | + | Western | `western` | `in0000191` | + | Western Epic | `western_epic` | `in0000189` | + | Whodunnit | `whodunnit` | `in0000141` | + | Witch Horror | `witch_horror` | `in0000121` | + | Workplace Drama | `workplace_drama` | `in0000091` | + | Wuxia | `wuxia` | `in0000200` | + | Zombie Horror | `zombie_horror` | `in0000122` | + + +### Example IMDb Search Builder(s) + +```yaml +collections: + IMDb Popular: + imdb_search: + type: movie + sort_by: popularity.asc + limit: 50 + collection_order: custom + sync_mode: sync +``` + +```yaml +collections: + Top Action: + imdb_search: + type: movie + release.after: 1990-01-01 + rating.gte: 5 + votes.gte: 100000 + genre: action + sort_by: rating.desc + limit: 100 +``` + +You can also find episodes using `imdb_search` like so. + +```yaml +collections: + The Simpsons Top 100 Episodes: + collection_order: custom + builder_level: episode + sync_mode: sync + imdb_search: + type: tv_episode + series: tt0096697 + sort_by: rating.desc + limit: 100 + summary: The top 100 Simpsons episodes by IMDb user rating +``` \ No newline at end of file diff --git a/docs/files/builders/imdb/watchlist.md b/docs/files/builders/imdb/watchlist.md new file mode 100644 index 000000000..5c778b36d --- /dev/null +++ b/docs/files/builders/imdb/watchlist.md @@ -0,0 +1,42 @@ +--- +hide: + - toc +--- +# IMDb Watchlist + +Finds every item in an IMDb User's Watchlist. + +| List Parameter | Description | +|:---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `user_id` | Specify the User ID for the IMDb Watchlist. **This attribute is required.**
**Options:** The ID that starts with `ur` found in the URL of the watchlist. (ex. `ur12345678`) | +| `limit` | Specify how items you want returned by the query.
**Options:** Any Integer `0` or greater where `0` get all items.
**Default:** `0` | +| `sort_by` | Choose from one of the many available sort options.
**Options:** `custom.asc`, `custom.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `popularity.asc`, `popularity.desc`, `votes.asc`, `votes.desc`, `release.asc`, `release.desc`, `runtime.asc`, `runtime.desc`, `added.asc`, `added.desc`
**Default:** `custom.asc` | + +Multiple values are supported as a list only a comma-separated string will not work. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example IMDb Watchlist Builder(s) + +```yaml +collections: + My Watch Watchlist: + imdb_watchlist: + user_id: ur64054558 + sort_by: rating.asc + collection_order: custom + sync_mode: sync +``` + +```yaml +collections: + My Friends Watchlists: + imdb_watchlist: + - user_id: ur64054558 + sort_by: rating.asc + limit: 100 + - user_id: ur12345678 + sort_by: rating.asc + limit: 100 + sync_mode: sync +``` diff --git a/docs/files/builders/letterboxd/list.md b/docs/files/builders/letterboxd/list.md new file mode 100644 index 000000000..997da71a3 --- /dev/null +++ b/docs/files/builders/letterboxd/list.md @@ -0,0 +1,58 @@ +--- +hide: + - toc +--- +# Letterboxd List + +Finds every movie in the Letterboxd list or [Letterboxd Films Search](https://letterboxd.com/films/). + +The expected input is a Letterboxd List URL or Letterboxd Film Search URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +You can add different filters directly to this Builder. + +| Filter Attribute | Description | +|:--------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | **Description:** Max number of items per returned.
**Values:** number greater than `1` | +| `rating` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-letterboxd-filters-1" } | **Description:** Search for the specified rating range. The rating is the list owner's rating not site wide rating.
**Values:** range of int i.e. `8-10` (convert Letterboxd stars to a 10 point scale) | +| `year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-letterboxd-filters-1" } | **Description:** Search for the specified year range.
**Values:** range of int i.e. `1990-1999` | +| `note` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-letterboxd-filters-2" } | **Description:** Search for the specified value in the note. The note is the list owner's note not site wide note.
**Values:** Any String | + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +Using the `limit` filter attribute is recommended when using a Letterboxd Film Search as the number of results returned could be very large. + +???+ tip "Details Builder" + + You can replace `letterboxd_list` with `letterboxd_list_details` if you would like to fetch and use the description from the list + + +### Example Letterboxd List Builder(s) + +```yaml +collections: + Vulture’s 101 Best Movie Endings: + letterboxd_list: https://letterboxd.com/brianformo/list/vultures-101-best-movie-endings/ + collection_order: custom + sync_mode: sync +``` + +* You can update the collection details with the Letterboxd List's description by using `letterboxd_list_details`. + * You can specify multiple collections in `letterboxd_list_details` but it will only use the first one to update the collection summary. + +```yaml +collections: + Vulture’s 101 Best Movie Endings: + letterboxd_list_details: https://letterboxd.com/brianformo/list/vultures-101-best-movie-endings/ + collection_order: custom + sync_mode: sync +``` + +```yaml +collections: + Vulture’s 101 Best Movie Endings From the 90s: + letterboxd_list_details: + url: https://letterboxd.com/brianformo/list/vultures-101-best-movie-endings/ + year: 1990-1999 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/letterboxd/overview.md b/docs/files/builders/letterboxd/overview.md new file mode 100644 index 000000000..81359cbc9 --- /dev/null +++ b/docs/files/builders/letterboxd/overview.md @@ -0,0 +1,12 @@ +--- +hide: + - toc +--- +# Letterboxd Builders + +You can find items using the lists on [Letterboxd.com](https://letterboxd.com/) (Letterboxd). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-----------------------------|:-----------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| +| [`letterboxd_list`](list.md) | Finds every movie in the Letterboxd List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + diff --git a/docs/files/builders/mdblist.md b/docs/files/builders/mdblist.md deleted file mode 100644 index f03f08d67..000000000 --- a/docs/files/builders/mdblist.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -hide: - - toc ---- -# MDBList Builders - -You can find items using the features of [MDBList.com](https://mdblist.com/) (MDBList). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:--------------------------------|:--------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`mdblist_list`](#mdblist-list) | Gets every movie/show in a [MDBList List](https://mdblist.com/toplists/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - -???+ important "Note on `mdb` sources" - - MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only periodically refreshed. As such, the data that Kometa fetches and applies from MDBList may not be the same as you see if you visit those third-party sources directly. - -=== "MDBList List" - - Finds every item in a [MDBList List](https://mdblist.com/toplists/). - - The expected input is an MDBList List URL. Multiple values are supported as a list only a comma-separated string will not work. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MDBList List Builder(s) - - ```yaml - collections: - Top Movies of The Week: - mdblist_list: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week - collection_order: custom - sync_mode: sync - ``` - You can also limit the number of items to search for by using the `limit` and `url` attributes under `mdblist_list`. - - ```yaml - collections: - Top 10 Movies of The Week: - mdblist_list: - url: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week - limit: 10 - collection_order: custom - sync_mode: sync - ``` - You can also sort the items by using the `sort_by` and `url` attributes under `mdblist_list`. - - The default `sort_by` when it's not specified is `rank.asc`. - -=== "Sort Options" - - For these sorts to be reflected in your collection you **must** use `collection_order: custom`. - - | Option | Description | - |:----------------------------------------------|:---------------------------------------| - | `rank.asc`
`rank.desc` | Sort by MDBList Rank | - | `score.asc`
`score.desc` | Sort by MDBList Score | - | `score_average.asc`
`score_average.desc` | Sort by MDBList Average Score | - | `released.asc`
`released.desc` | Sort by Release Date | - | `releasedigital.asc`
`releasedigital.desc` | Sort by Digital Release Date | - | `imdbrating.asc`
`imdbrating.desc` | Sort by IMDb Rating | - | `imdbvotes.asc`
`imdbvotes.desc` | Sort by IMDb Votes | - | `imdbpopular.asc`
`imdbpopular.desc` | Sort by IMDb Popular | - | `tmdbpopular.asc`
`tmdbpopular.desc` | Sort by TMDb Popular | - | `rogerebert.asc`
`rogerebert.desc` | Sort by RogerEbert Score | - | `rtomatoes.asc`
`rtomatoes.desc` | Sort by Rotten Tomatoes Score | - | `rtaudience.asc`
`rtaudience.desc` | Sort by Rotten Tomatoes Audience Score | - | `metacritic.asc`
`metacritic.desc` | Sort by Metacritic Score | - | `myanimelist.asc`
`myanimelist.desc` | Sort by MyAnimeList Score | - | `letterrating.asc`
`letterrating.desc` | Sort by Letterboxd Rating | - | `lettervotes.asc`
`lettervotes.desc` | Sort by Letterboxd Votes | - | `last_air_date.asc`
`last_air_date.desc` | Sort by Last Air Date | - | `watched.asc`
`watched.desc` | Sort by Last Watched Date | - | `rating.asc`
`rating.desc` | Sort by Users Rating | - | `download.asc`
`download.desc` | Sort by Downloaded | - | `usort.asc`
`usort.desc` | Sort by User Sort | - | `added.asc`
`added.desc` | Sort by Date Added | - | `runtime.asc`
`runtime.desc` | Sort by Runtime | - | `budget.asc`
`budget.desc` | Sort by Budget | - | `revenue.asc`
`revenue.desc` | Sort by Revenue | - | `title.asc`
`title.desc` | Sort by Title | - | `random.asc`
`random.desc` | Sort by Random (Randomized Daily) | - - ### Example MDBList List Builder(s) with Sort Option - - ```yaml - collections: - Top 10 Movies of The Week: - mdblist_list: - url: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week - sort_by: imdbrating.desc - collection_order: custom - sync_mode: sync - ``` diff --git a/docs/files/builders/mdblist/list.md b/docs/files/builders/mdblist/list.md new file mode 100644 index 000000000..93f113a2a --- /dev/null +++ b/docs/files/builders/mdblist/list.md @@ -0,0 +1,84 @@ +--- +hide: + - toc +--- +# MDBList List + + +???+ tip "Note on `mdb` sources" + + MDBList is not a live reflection of third-party sites such as CommonSense and Trakt. The data on MDBList is often days, weeks and months out of date as it is only periodically refreshed. As such, the data that Kometa fetches and applies from MDBList may not be the same as you see if you visit those third-party sources directly. + +Finds every item in a [MDBList List](https://mdblist.com/toplists/). + +The expected input is an MDBList List URL. Multiple values are supported as a list only a comma-separated string will not work. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +## Sort Options + +For these sorts to be reflected in your collection you **must** use `collection_order: custom`. + +| Option | Description | +|:----------------------------------------------|:---------------------------------------| +| `rank.asc`
`rank.desc` | Sort by MDBList Rank | +| `score.asc`
`score.desc` | Sort by MDBList Score | +| `score_average.asc`
`score_average.desc` | Sort by MDBList Average Score | +| `released.asc`
`released.desc` | Sort by Release Date | +| `releasedigital.asc`
`releasedigital.desc` | Sort by Digital Release Date | +| `imdbrating.asc`
`imdbrating.desc` | Sort by IMDb Rating | +| `imdbvotes.asc`
`imdbvotes.desc` | Sort by IMDb Votes | +| `imdbpopular.asc`
`imdbpopular.desc` | Sort by IMDb Popular | +| `tmdbpopular.asc`
`tmdbpopular.desc` | Sort by TMDb Popular | +| `rogerebert.asc`
`rogerebert.desc` | Sort by RogerEbert Score | +| `rtomatoes.asc`
`rtomatoes.desc` | Sort by Rotten Tomatoes Score | +| `rtaudience.asc`
`rtaudience.desc` | Sort by Rotten Tomatoes Audience Score | +| `metacritic.asc`
`metacritic.desc` | Sort by Metacritic Score | +| `myanimelist.asc`
`myanimelist.desc` | Sort by MyAnimeList Score | +| `letterrating.asc`
`letterrating.desc` | Sort by Letterboxd Rating | +| `lettervotes.asc`
`lettervotes.desc` | Sort by Letterboxd Votes | +| `last_air_date.asc`
`last_air_date.desc` | Sort by Last Air Date | +| `watched.asc`
`watched.desc` | Sort by Last Watched Date | +| `rating.asc`
`rating.desc` | Sort by Users Rating | +| `download.asc`
`download.desc` | Sort by Downloaded | +| `usort.asc`
`usort.desc` | Sort by User Sort | +| `added.asc`
`added.desc` | Sort by Date Added | +| `runtime.asc`
`runtime.desc` | Sort by Runtime | +| `budget.asc`
`budget.desc` | Sort by Budget | +| `revenue.asc`
`revenue.desc` | Sort by Revenue | +| `title.asc`
`title.desc` | Sort by Title | +| `random.asc`
`random.desc` | Sort by Random (Randomized Daily) | + +### Example MDBList List Builder(s) + +```yaml +collections: + Top Movies of The Week: + mdblist_list: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week + collection_order: custom + sync_mode: sync +``` +You can also limit the number of items to search for by using the `limit` and `url` attributes under `mdblist_list`. + +```yaml +collections: + Top 10 Movies of The Week: + mdblist_list: + url: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week + limit: 10 + collection_order: custom + sync_mode: sync +``` +You can also sort the items by using the `sort_by` and `url` attributes under `mdblist_list`. + +The default `sort_by` when it's not specified is `rank.asc`. + +```yaml +collections: + Top 10 Movies of The Week: + mdblist_list: + url: https://mdblist.com/lists/linaspurinis/top-watched-movies-of-the-week + sort_by: imdbrating.desc + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/mdblist/overview.md b/docs/files/builders/mdblist/overview.md new file mode 100644 index 000000000..a050bd54b --- /dev/null +++ b/docs/files/builders/mdblist/overview.md @@ -0,0 +1,11 @@ +--- +hide: + - toc +--- +# MDBList Builders + +You can find items using the features of [MDBList.com](https://mdblist.com/) (MDBList). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:--------------------------|:--------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`mdblist_list`](list.md) | Gets every movie/show in a [MDBList List](https://mdblist.com/toplists/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | diff --git a/docs/files/builders/mojo.md b/docs/files/builders/mojo.md deleted file mode 100644 index 04449b34b..000000000 --- a/docs/files/builders/mojo.md +++ /dev/null @@ -1,266 +0,0 @@ ---- -hide: - - toc ---- -# BoxOfficeMojo Builders - -You can find items using the lists on [boxofficemojo.com](https://www.boxofficemojo.com/) (BoxOfficeMojo). - - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:---------------------------------------|:-----------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| -| [`mojo_domestic`](#domestic) | Uses the Domestic Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`mojo_international`](#international) | Uses the International Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`mojo_world`](#worldwide) | Uses the Worldwide Box Office. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`mojo_all_time`](#all-time) | Uses the All Time lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`mojo_never`](#never-hit) | Uses the Never Hit lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`mojo_record`](#other-records) | Uses other Record lists. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .red } | :fontawesome-solid-circle-check:{ .green } | - - -=== "Domestic" - - Uses the Domestic Box Office to collect items. - - **Builder Attribute:** `mojo_domestic` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `range` | Determines the type of time range of the Box Office.
**Allowed Values:** `daily`, `weekend`, `weekly`, `monthly`, `quarterly`, `yearly`, `season`, or `holiday` | - | `year` | Determines the year of the Box Office. This attribute is ignored for the `daily` range.
**Default Value:** `current`
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | - | `range_data` | Determines the actual time range of the Box Office. The input changes depending on the value of `range`.
**Required:** Yes, except for `yearly` range | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - === "Allowed Values for `range_data`" - - | Range | Allowed Values | - |-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `daily` | Date in the format `MM-DD-YYYY`, `current`, or relative (`current-#`) where `#` is days before current | - | `weekend` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | - | `weekly` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | - | `monthly` | `january`, `february`, ..., `december`, `current`, or relative (`current-#`) where `#` is days before current | - | `quarterly` | `q1`, `q2`, `q3`, `q4`, `current`, or relative (`current-#`) where `#` is days before current | - | `season` | `winter`, `spring`, `summer`, `fall`, `holiday`, or `current` | - | `holiday` | `new_years_day`, `new_year_weekend`, `mlk_day`, `mlk_day_weekend`, `presidents_day`, `presidents_day_weekend`, `easter`, `easter_weekend`, `memorial_day`, `memorial_day_weekend`, `independence_day`, `independence_day_weekend`, `labor_day`, `labor_day_weekend`, `indigenous_day`, `indigenous_day_weekend`, `halloween`, `thanksgiving`, `thanksgiving_3`, `thanksgiving_4`, `thanksgiving_5`, `post_thanksgiving_weekend`, `christmas_day`, `christmas_weekend`, `new_years_eve` | - - ### Example Mojo Domestic Builder(s) - - ```yaml - collections: - Current Domestic Box Office: - mojo_domestic: - range: yearly - year: current - - Last Year's Domestic Box Office: - mojo_domestic: - range: yearly - year: current-1 - - Last Month's Top 10 Domestic Box Office: - mojo_domestic: - range: monthly - range_data: current - year: current-1 - limit: 10 - ``` - -=== "International" - - Uses the International Box Office to collect items. - - **Builder Attribute:** `mojo_international` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `range` | Determines the type of time range of the Box Office.
**Allowed Values:** `weekend`, `monthly`, `quarterly`, or `yearly` | - | `chart` | Determines the chart you want to use.
**Default Value:** `international`
**Allowed Values:** Item in the dropdown found [here](https://www.boxofficemojo.com/intl/) | - | `year` | Determines the year of the Box Office.
**Default Value:** `current`
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | - | `range_data` | Determines the actual time range of the Box Office. Required for all ranges except `yearly`. Input depends on the `range` selected. | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - === "Allowed Values for `range_data`" - - | Range | Allowed Values | - |-------------|---------------------------------------------------------------------------------------------------------------| - | `weekend` | Week number (1–53), `current`, or relative (`current-#`) where `#` is days before current | - | `monthly` | `january`, `february`, ..., `december`, `current`, or relative (`current-#`) where `#` is days before current | - | `quarterly` | `q1`, `q2`, `q3`, `q4`, `current`, or relative (`current-#`) where `#` is days before current | - - ### Example Mojo International Builder(s) - - ```yaml - collections: - - Current International Box Office: - mojo_international: - range: yearly - year: current - - Last Year's International Box Office: - mojo_international: - range: yearly - year: current-1 - - Last Month's Top 10 German Box Office: - mojo_international: - range: monthly - range_data: current - chart: germany - year: current-1 - limit: 10 - ``` - -=== "Worldwide" - - Uses the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to collect items. - - **Builder Attribute:** `mojo_world` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `year` | The year of the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to pull.
**Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`) | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - ### Example Mojo Worldwide Builder(s) - - ```yaml - collections: - - Current Worldwide Box Office: - mojo_world: - year: current - - Last Year's Worldwide Box Office: - mojo_world: - year: current-1 - - 2020 Top 10 Worldwide Box Office: - mojo_world: - year: 2020 - limit: 10 - ``` - -=== "All Time" - - Uses the [All Time Lists](https://www.boxofficemojo.com/charts/overall/) to collect items. - - **Builder Attribute:** `mojo_all_time` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |-------------------------|---------------------------------------------------------------------------------------------------------------------------------| - | `chart` | Determines the chart you want to use.
**Allowed Values:** `domestic` or `worldwide` | - | `content_rating_filter` | Determines the content rating chart to use.
**Allowed Values:** `g`, `g/pg`, `pg`, `pg-13`, `r`, or `nc-17` | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - ### Example Mojo All Time Builder(s) - - ```yaml - collections: - - Top 100 Domestic All Time Grosses: - mojo_all_time: - chart: domestic - limit: 100 - - Top 100 Worldwide All Time Grosses: - mojo_all_time: - chart: worldwide - limit: 100 - - Top 10 Domestic All Time G Movie Grosses: - mojo_all_time: - chart: domestic - content_rating_filter: g - limit: 10 - ``` - -=== "Never Hit" - - Uses the [Never Hit Lists](https://www.boxofficemojo.com/charts/overall/) (Bottom Section) to collect items. - - **Builder Attribute:** `mojo_never` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |-----------|-----------------------------------------------------------------------------------------------------------------------------------------------| - | `chart` | Determines the chart you want to use.
**Allowed Values:** Item in the dropdown found [here](https://www.boxofficemojo.com/charts/overall/) | - | `never` | Determines the never filter to use.
**Default Value:** `1`
**Allowed Values:** `1`, `5`, or `10` | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - ### Example Mojo Never Hit Builder(s) - - ```yaml - collections: - - "Top 100 Domestic Never #1": - mojo_never: - chart: domestic - limit: 100 - - "Top 100 Domestic Never #10": - mojo_never: - chart: domestic - never: 10 - limit: 100 - - "Top 100 German Never #1": - mojo_never: - chart: germany - limit: 100 - ``` - -=== "Other Records" - - Uses the [Weekend Records](https://www.boxofficemojo.com/charts/weekend/), [Daily Records](https://www.boxofficemojo.com/charts/daily/), - and [Miscellaneous Records](https://www.boxofficemojo.com/charts/misc/) to collect items. - - **Builder Attribute:** `mojo_record` - - **Builder Value:** [Dictionary](../../kometa/yaml.md#dictionaries) of Attributes - - === "Builder Attributes" - - | Attribute | Description | - |-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `chart` | Determines the record you want to use.
**Allowed Values:**
`second_weekend_drop`, `post_thanksgiving_weekend_drop`, `top_opening_weekend`, `worst_opening_weekend_theater_avg`, `mlk_opening`, `easter_opening`, `memorial_opening`, `labor_opening`, `president_opening`, `thanksgiving_3_opening`, `thanksgiving_5_opening`, `mlk`, `easter`, `4th`, `memorial`, `labor`, `president`, `thanksgiving_3`, `thanksgiving_5`, `january`, `february`, `march`, `april`, `may`, `june`, `july`, `august`, `september`, `october`, `november`, `december`, `spring`, `summer`, `fall`, `holiday_season`, `winter`, `g`, `g/pg`, `pg`, `pg-13`, `r`, `nc-17`, `top_opening_weekend_theater_avg_all`, `top_opening_weekend_theater_avg_wide`, `opening_day`, `single_day_grosses`, `christmas_day_gross`, `new_years_day_gross`, `friday`, `saturday`, `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday_non_opening`, `saturday_non_opening`, `sunday_non_opening`, `monday_non_opening`, `tuesday_non_opening`, `wednesday_non_opening`, `thursday_non_opening`, `biggest_theater_drop`, `opening_week` | - | `limit` | The maximum number of results to return.
**Default Value:** Returns all results
**Allowed Values:** Number greater than 0 | - - ### Example Mojo Other Records Builder(s) - - ```yaml - collections: - - Top 10 Biggest Opening Weekends: - mojo_record: - chart: top_opening_weekend - limit: 10 - - Top 10 Biggest Opening Day: - mojo_record: - chart: opening_day - limit: 10 - - Top 10 Biggest Opening Weeks: - mojo_record: - chart: opening_week - limit: 10 - ``` \ No newline at end of file diff --git a/docs/files/builders/myanimelist.md b/docs/files/builders/myanimelist.md deleted file mode 100644 index b760239a1..000000000 --- a/docs/files/builders/myanimelist.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -hide: - - toc ---- -# MyAnimeList Builders - -You can find anime using the features of [MyAnimeList.net](https://myanimelist.net/) (MyAnimeList). - -???+ warning "MyAnimeList Configuration" - - [Configuring MyAnimeList](../../config/myanimelist.md) in the config is required for any of these builders to function. - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:---------------------------------------------|:------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`mal_search`](#myanimelist-search) | Finds every anime in a MyAnimeList Search list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_all`](#myanimelist-top-all) | Finds every anime in MyAnimeList's [Top All Anime](https://myanimelist.net/topanime.php) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_airing`](#myanimelist-top-airing) | Finds every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_upcoming`](#myanimelist-top-upcoming) | Finds every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_tv`](#myanimelist-top-tv) | Finds every anime in MyAnimeList's [Top Anime TV Series](https://myanimelist.net/topanime.php?type=tv) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_movie`](#myanimelist-top-movie) | Finds every anime in MyAnimeList's [Top Anime Movies](https://myanimelist.net/topanime.php?type=movie) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_ova`](#myanimelist-top-ova) | Finds every anime in MyAnimeList's [Top Anime OVA Series](https://myanimelist.net/topanime.php?type=ova) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_special`](#myanimelist-top-special) | Finds every anime in MyAnimeList's [Top Anime Specials](https://myanimelist.net/topanime.php?type=special) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_popular`](#myanimelist-most-popular) | Finds every anime in MyAnimeList's [Most Popular Anime](https://myanimelist.net/topanime.php?type=bypopularity) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_favorite`](#myanimelist-most-favorite) | Finds every anime in MyAnimeList's [Most Favorited Anime](https://myanimelist.net/topanime.php?type=favorite) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_suggested`](#myanimelist-suggested) | Finds the suggested anime in by MyAnimeList for the authorized user | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_id`](#myanimelist-id) | Finds the anime specified by the MyAnimeList ID | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`mal_userlist`](#myanimelist-userlist) | Finds anime in MyAnimeList User's Anime list the options are detailed below | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`mal_season`](#myanimelist-seasonal) | Finds anime in MyAnimeList's [Seasonal Anime](https://myanimelist.net/anime/season) list the options are detailed below | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - -=== "MyAnimeList Search" - - Gets every anime in a MyAnimeList search. The different sub-attributes are detailed below. At least one attribute is required. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - | Attribute | Description | - |:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `sort_by` | **Description:** Sort Order to return
**Values:** `mal_id.desc`, `mal_id.asc`, `title.desc`, `title.asc`, `type.desc`, `type.asc`, `rating.desc`, `rating.asc`, `start_date.desc`, `start_date.asc`, `end_date.desc`, `end_date.asc`, `episodes.desc`, `episodes.asc`, `score.desc`, `score.asc`, `scored_by.desc`, `scored_by.asc`, `rank.desc`, `rank.asc`, `popularity.desc`, `popularity.asc`, `members.desc`, `members.asc`, `favorites.desc`, `favorites.asc` | - | `limit` | **Description:** Don't return more than this number
**Values:** Number of Anime to query from MyAnimeList | - | `query` | **Description:** Text query to search for | - | `prefix` | **Description:** Results must begin with this prefix | - | `type` | **Description:** Type of Anime to search for
**Values:** `tv`, `movie`, `ova`, `special`, `ona`, `music` | - | `status` | **Description:** Status to search for
**Values:** `airing`, `complete`, `upcoming` | - | `genre` | **Description:** Comma-separated String of Genres to include using `,` for `AND` and | for `OR`
**Values:** Genre Name or ID | - | `genre.not` | **Description:** Comma-separated String of Genres to exclude using `,` for `AND` and | for `OR`
**Values:** Genre Name or ID | - | `studio` | **Description:** Comma-separated String of Genres to include using `,` for `AND` and | for `OR`
**Values:** Studio Name or ID | - | `content_rating` | **Description:** Content Rating to search for
**Values:** `g`, `pg`, `pg13`, `r17`, `r`, `rx` | - | `score.gt`/`score.gte` | **Description:** Score must be greater than the given number
**Values:** Float between `0.00`-`10.00` | - | `score.lt`/`score.lte` | **Description:** Score must be less than the given number
**Values:** Float between `0.00`-`10.00` | - | `sfw` | **Description:** Results must be Safe for Work
**Value:** `true` | - - * Studio options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. - * Genre options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. - * To find the ID click on a Studio or Genre in the link above and there should be a number in the URL that's the `id`. - * For example if the url is `https://myanimelist.net/anime/producer/4/Bones` the `id` would be `4` or if the url is `https://myanimelist.net/anime/genre/1/Action` the `id` would be `1`. - - ### Example MyAnimeList Search Builder(s) - - ```yaml - collections: - Top Action Anime: - mal_search: - limit: 100 - sort_by: score.desc - genre: Action - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top All" - - Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top All Builder(s) - - ```yaml - collections: - Top All Anime: - mal_all: 30 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top Airing" - - Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top Airing Builder(s) - - ```yaml - collections: - Top Airing Anime: - mal_airing: 10 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top Upcoming" - - Gets every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top Upcoming Builder(s) - - ```yaml - collections: - Top Upcoming Anime: - mal_upcoming: 10 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top TV Series" - - Gets every anime in MyAnimeList's [Top Anime TV Series](https://myanimelist.net/topanime.php?type=tv) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top TV Series Builder(s) - - ```yaml - collections: - Top Anime TV Series: - mal_tv: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top Movies" - - Gets every anime in MyAnimeList's [Top Anime Movies](https://myanimelist.net/topanime.php?type=movie) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top Movies Builder(s) - - ```yaml - collections: - Top Anime Movies: - mal_movie: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top OVA Series" - - Gets every anime in MyAnimeList's [Top Anime OVA Series](https://myanimelist.net/topanime.php?type=ova) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top OVA Series Builder(s) - - ```yaml - collections: - Top Anime OVA Series: - mal_ova: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Top Specials" - - Gets every anime in MyAnimeList's [Top Anime Specials](https://myanimelist.net/topanime.php?type=special) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Top Specials Builder(s) - - ```yaml - collections: - Top Anime Specials: - mal_special: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Most Popular" - - Gets every anime in MyAnimeList's [Most Popular Anime](https://myanimelist.net/topanime.php?type=bypopularity) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Most Popular Builder(s) - - ```yaml - collections: - Most Popular Anime: - mal_popular: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Most Favorited" - - Gets every anime in MyAnimeList's [Most Favorited Anime](https://myanimelist.net/topanime.php?type=favorite) list. (Maximum: 500) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Most Favorited Builder(s) - - ```yaml - collections: - Most Favorited Anime: - mal_favorite: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Suggested" - - Gets the suggested anime in by MyAnimeList for the authorized user. (Maximum: 100) - - The expected input value is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - ### Example MyAnimeList Suggested Builder(s) - - ```yaml - collections: - Suggested Anime: - mal_suggested: 20 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList ID" - - Gets the anime specified by the MyAnimeList ID. - - The expected input is a MyAnimeList ID. Multiple values are supported as either a list or a comma-separated string. - - ### Example MyAnimeList ID Builder(s) - - ```yaml - collections: - Cowboy Bebop: - mal_id: 23, 219 - ``` - -=== "MyAnimeList UserList" - - Gets anime in MyAnimeList User's Anime list. The different sub-attributes are detailed below. The only required attribute is `username` - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - | Attribute | Description | - |:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `username` | **Description:** A user's MyAnimeList Username or `@me` for the authorized user | - | `status` | **Description:** Status to search for
**Default:** `all`
**Values:**
`all`All Anime List
`watching`Currently Watching List
`completed`Completed List
`on_hold`On Hold List
`dropped`Dropped List
`plan_to_watch`Plan to Watch
| - | `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by Score
`last_updated`Sort by Last Updated
`title`Sort by Anime Title
`start_date`Sort by Start Date
| - | `limit` | **Description:** Don't return more than this number
**Default:** `100`
**Values:** Number of Anime to query from MyAnimeList (max: 1000) | - - ### Example MyAnimeList UserList Builder(s) - - ```yaml - collections: - Currently Watching Anime: - mal_userlist: - username: "@me" - status: watching - sort_by: score - limit: 500 - collection_order: custom - sync_mode: sync - ``` - -=== "MyAnimeList Seasonal" - - Gets anime in MyAnimeList's [Seasonal Anime](https://myanimelist.net/anime/season) list the options are detailed below. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - - | Attribute | Description & Values | - |:----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `season` | **Description:** Season to search
**Default:** `current`
**Values:**
`winter`For winter season January, February, March
`spring`For spring season April, May, June
`summer`For summer season July, August, September
`fall`For fall season October, November, December
`current`For the current Season
| - | `year` | **Description:** Year to search
**Default:** Current Year
**Values:** Number between `1917` and the current year. | - | `sort_by` | **Description:** Sort Order
**Default:** `members`
**Values:**
`members`Sort by Most Members
`score`Sort by Score
| - | `limit` | **Description:** Don't return more than this number
**Default:** `100`
**Values:** Number of Anime to query from MyAnimeList (max: 500) | - | `starting_only` | **Description:** Return only anime that began airing in the selected season
**Default:** `False`
**Values:** `True` or `False` | - - ### Example MyAnimeList Seasonal Builder(s) - - ```yaml - collections: - Current Anime Season: - mal_season: - sort_by: members - limit: 50 - collection_order: custom - sync_mode: sync - ``` - ```yaml - collections: - Fall 2020 Anime: - mal_season: - season: fall - year: 2020 - limit: 50 - collection_order: custom - sync_mode: sync - ``` diff --git a/docs/files/builders/myanimelist/airing.md b/docs/files/builders/myanimelist/airing.md new file mode 100644 index 000000000..8baab326c --- /dev/null +++ b/docs/files/builders/myanimelist/airing.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top Airing + +Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top Airing Builder(s) + +```yaml +collections: + Top Airing Anime: + mal_airing: 10 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/all.md b/docs/files/builders/myanimelist/all.md new file mode 100644 index 000000000..533c78a5c --- /dev/null +++ b/docs/files/builders/myanimelist/all.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top All + +Gets every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top All Builder(s) + +```yaml +collections: + Top All Anime: + mal_all: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/favorite.md b/docs/files/builders/myanimelist/favorite.md new file mode 100644 index 000000000..1451a5b68 --- /dev/null +++ b/docs/files/builders/myanimelist/favorite.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Most Favorited + +Gets every anime in MyAnimeList's [Most Favorited Anime](https://myanimelist.net/topanime.php?type=favorite) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Most Favorited Builder(s) + +```yaml +collections: + Most Favorited Anime: + mal_favorite: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/id.md b/docs/files/builders/myanimelist/id.md new file mode 100644 index 000000000..5f51f8c19 --- /dev/null +++ b/docs/files/builders/myanimelist/id.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# MyAnimeList ID + +Gets the anime specified by the MyAnimeList ID. + +The expected input is a MyAnimeList ID. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList ID Builder(s) + +```yaml +collections: + Cowboy Bebop: + mal_id: 23, 219 +``` diff --git a/docs/files/builders/myanimelist/movie.md b/docs/files/builders/myanimelist/movie.md new file mode 100644 index 000000000..ec7d56587 --- /dev/null +++ b/docs/files/builders/myanimelist/movie.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top Movies + +Gets every anime in MyAnimeList's [Top Anime Movies](https://myanimelist.net/topanime.php?type=movie) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top Movies Builder(s) + +```yaml +collections: + Top Anime Movies: + mal_movie: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/ova.md b/docs/files/builders/myanimelist/ova.md new file mode 100644 index 000000000..b742cfda5 --- /dev/null +++ b/docs/files/builders/myanimelist/ova.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top OVA Series + +Gets every anime in MyAnimeList's [Top Anime OVA Series](https://myanimelist.net/topanime.php?type=ova) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top OVA Series Builder(s) + +```yaml +collections: + Top Anime OVA Series: + mal_ova: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/overview.md b/docs/files/builders/myanimelist/overview.md new file mode 100644 index 000000000..d72c0b53d --- /dev/null +++ b/docs/files/builders/myanimelist/overview.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# MyAnimeList Builders + +You can find anime using the features of [MyAnimeList.net](https://myanimelist.net/) (MyAnimeList). + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`mal_airing`](airing.md) | Finds every anime in MyAnimeList's [Top Airing Anime](https://myanimelist.net/topanime.php?type=airing) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_all`](all.md) | Finds every anime in MyAnimeList's [Top All Anime](https://myanimelist.net/topanime.php) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_favorite`](favorite.md) | Finds every anime in MyAnimeList's [Most Favorited Anime](https://myanimelist.net/topanime.php?type=favorite) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_id`](id.md) | Finds the anime specified by the MyAnimeList ID | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`mal_movie`](movie.md) | Finds every anime in MyAnimeList's [Top Anime Movies](https://myanimelist.net/topanime.php?type=movie) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_ova`](ova.md) | Finds every anime in MyAnimeList's [Top Anime OVA Series](https://myanimelist.net/topanime.php?type=ova) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_popular`](popular.md) | Finds every anime in MyAnimeList's [Most Popular Anime](https://myanimelist.net/topanime.php?type=bypopularity) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_search`](search.md) | Finds every anime in a MyAnimeList Search list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_season`](season.md) | Finds anime in MyAnimeList's [Seasonal Anime](https://myanimelist.net/anime/season) list the options are detailed below | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_special`](special.md) | Finds every anime in MyAnimeList's [Top Anime Specials](https://myanimelist.net/topanime.php?type=special) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_suggested`](suggested.md) | Finds the suggested anime in by MyAnimeList for the authorized user | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_tv`](tv.md) | Finds every anime in MyAnimeList's [Top Anime TV Series](https://myanimelist.net/topanime.php?type=tv) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_upcoming`](upcoming.md) | Finds every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`mal_userlist`](userlist.md) | Finds anime in MyAnimeList User's Anime list the options are detailed below | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + diff --git a/docs/files/builders/myanimelist/popular.md b/docs/files/builders/myanimelist/popular.md new file mode 100644 index 000000000..c1e7f07bb --- /dev/null +++ b/docs/files/builders/myanimelist/popular.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Most Popular + +Gets every anime in MyAnimeList's [Most Popular Anime](https://myanimelist.net/topanime.php?type=bypopularity) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Most Popular Builder(s) + +```yaml +collections: + Most Popular Anime: + mal_popular: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/search.md b/docs/files/builders/myanimelist/search.md new file mode 100644 index 000000000..d2baa7a88 --- /dev/null +++ b/docs/files/builders/myanimelist/search.md @@ -0,0 +1,47 @@ +--- +hide: + - toc +--- +# MyAnimeList Search + +Gets every anime in a MyAnimeList search. The different sub-attributes are detailed below. At least one attribute is required. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +| Attribute | Description | +|:-----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `sort_by` | **Description:** Sort Order to return
**Values:** `mal_id.desc`, `mal_id.asc`, `title.desc`, `title.asc`, `type.desc`, `type.asc`, `rating.desc`, `rating.asc`, `start_date.desc`, `start_date.asc`, `end_date.desc`, `end_date.asc`, `episodes.desc`, `episodes.asc`, `score.desc`, `score.asc`, `scored_by.desc`, `scored_by.asc`, `rank.desc`, `rank.asc`, `popularity.desc`, `popularity.asc`, `members.desc`, `members.asc`, `favorites.desc`, `favorites.asc` | +| `limit` | **Description:** Don't return more than this number
**Values:** Number of Anime to query from MyAnimeList | +| `query` | **Description:** Text query to search for | +| `prefix` | **Description:** Results must begin with this prefix | +| `type` | **Description:** Type of Anime to search for
**Values:** `tv`, `movie`, `ova`, `special`, `ona`, `music` | +| `status` | **Description:** Status to search for
**Values:** `airing`, `complete`, `upcoming` | +| `genre` | **Description:** Comma-separated String of Genres to include using `,` for `AND` and | for `OR`
**Values:** Genre Name or ID | +| `genre.not` | **Description:** Comma-separated String of Genres to exclude using `,` for `AND` and | for `OR`
**Values:** Genre Name or ID | +| `studio` | **Description:** Comma-separated String of Genres to include using `,` for `AND` and | for `OR`
**Values:** Studio Name or ID | +| `content_rating` | **Description:** Content Rating to search for
**Values:** `g`, `pg`, `pg13`, `r17`, `r`, `rx` | +| `score.gt`/`score.gte` | **Description:** Score must be greater than the given number
**Values:** Float between `0.00`-`10.00` | +| `score.lt`/`score.lte` | **Description:** Score must be less than the given number
**Values:** Float between `0.00`-`10.00` | +| `sfw` | **Description:** Results must be Safe for Work
**Value:** `true` | + +* Studio options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. +* Genre options can be found on [MyAnimeList's Search](https://myanimelist.net/anime.php) Page. +* To find the ID click on a Studio or Genre in the link above and there should be a number in the URL that's the `id`. +* For example if the url is `https://myanimelist.net/anime/producer/4/Bones` the `id` would be `4` or if the url is `https://myanimelist.net/anime/genre/1/Action` the `id` would be `1`. + +### Example MyAnimeList Search Builder(s) + +```yaml +collections: + Top Action Anime: + mal_search: + limit: 100 + sort_by: score.desc + genre: Action + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/season.md b/docs/files/builders/myanimelist/season.md new file mode 100644 index 000000000..c32a7a4bf --- /dev/null +++ b/docs/files/builders/myanimelist/season.md @@ -0,0 +1,43 @@ +--- +hide: + - toc +--- +# MyAnimeList Seasonal + +Gets anime in MyAnimeList's [Seasonal Anime](https://myanimelist.net/anime/season) list the options are detailed below. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +| Attribute | Description & Values | +|:----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `season` | **Description:** Season to search
**Default:** `current`
**Values:**
`winter`For winter season January, February, March
`spring`For spring season April, May, June
`summer`For summer season July, August, September
`fall`For fall season October, November, December
`current`For the current Season
| +| `year` | **Description:** Year to search
**Default:** Current Year
**Values:** Number between `1917` and the current year. | +| `sort_by` | **Description:** Sort Order
**Default:** `members`
**Values:**
`members`Sort by Most Members
`score`Sort by Score
| +| `limit` | **Description:** Don't return more than this number
**Default:** `100`
**Values:** Number of Anime to query from MyAnimeList (max: 500) | +| `starting_only` | **Description:** Return only anime that began airing in the selected season
**Default:** `False`
**Values:** `True` or `False` | + +### Example MyAnimeList Seasonal Builder(s) + +```yaml +collections: + Current Anime Season: + mal_season: + sort_by: members + limit: 50 + collection_order: custom + sync_mode: sync +``` +```yaml +collections: + Fall 2020 Anime: + mal_season: + season: fall + year: 2020 + limit: 50 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/special.md b/docs/files/builders/myanimelist/special.md new file mode 100644 index 000000000..4e115651b --- /dev/null +++ b/docs/files/builders/myanimelist/special.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top Specials + +Gets every anime in MyAnimeList's [Top Anime Specials](https://myanimelist.net/topanime.php?type=special) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top Specials Builder(s) + +```yaml +collections: + Top Anime Specials: + mal_special: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/suggested.md b/docs/files/builders/myanimelist/suggested.md new file mode 100644 index 000000000..8c8482b55 --- /dev/null +++ b/docs/files/builders/myanimelist/suggested.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Suggested + +Gets the suggested anime in by MyAnimeList for the authorized user. (Maximum: 100) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Suggested Builder(s) + +```yaml +collections: + Suggested Anime: + mal_suggested: 20 + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/myanimelist/tv.md b/docs/files/builders/myanimelist/tv.md new file mode 100644 index 000000000..578473ab3 --- /dev/null +++ b/docs/files/builders/myanimelist/tv.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top TV Series + +Gets every anime in MyAnimeList's [Top Anime TV Series](https://myanimelist.net/topanime.php?type=tv) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top TV Series Builder(s) + +```yaml +collections: + Top Anime TV Series: + mal_tv: 20 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/upcoming.md b/docs/files/builders/myanimelist/upcoming.md new file mode 100644 index 000000000..e7a521b89 --- /dev/null +++ b/docs/files/builders/myanimelist/upcoming.md @@ -0,0 +1,25 @@ +--- +hide: + - toc +--- +# MyAnimeList Top Upcoming + +Gets every anime in MyAnimeList's [Top Upcoming Anime](https://myanimelist.net/topanime.php?type=upcoming) list. (Maximum: 500) + +The expected input value is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +### Example MyAnimeList Top Upcoming Builder(s) + +```yaml +collections: + Top Upcoming Anime: + mal_upcoming: 10 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/myanimelist/userlist.md b/docs/files/builders/myanimelist/userlist.md new file mode 100644 index 000000000..b164d21ac --- /dev/null +++ b/docs/files/builders/myanimelist/userlist.md @@ -0,0 +1,34 @@ +--- +hide: + - toc +--- +# MyAnimeList UserList + +Gets anime in MyAnimeList User's Anime list. The different sub-attributes are detailed below. The only required attribute is `username` + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +???+ warning "MyAnimeList Configuration" + + [Configuring MyAnimeList](../../../config/myanimelist.md) in the config is required for any of these builders to function. + +| Attribute | Description | +|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `username` | **Description:** A user's MyAnimeList Username or `@me` for the authorized user | +| `status` | **Description:** Status to search for
**Default:** `all`
**Values:**
`all`All Anime List
`watching`Currently Watching List
`completed`Completed List
`on_hold`On Hold List
`dropped`Dropped List
`plan_to_watch`Plan to Watch
| +| `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by Score
`last_updated`Sort by Last Updated
`title`Sort by Anime Title
`start_date`Sort by Start Date
| +| `limit` | **Description:** Don't return more than this number
**Default:** `100`
**Values:** Number of Anime to query from MyAnimeList (max: 1000) | + +### Example MyAnimeList UserList Builder(s) + +```yaml +collections: + Currently Watching Anime: + mal_userlist: + username: "@me" + status: watching + sort_by: score + limit: 500 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/overview.md b/docs/files/builders/overview.md index 3578749c0..fc95432d4 100644 --- a/docs/files/builders/overview.md +++ b/docs/files/builders/overview.md @@ -16,8 +16,8 @@ Builders use third-party services to source items to be added to the collection. ??? quicklink "Popular Builders" - - [:simple-plex: Smart Filter](plex.md#smart-filter) - Create a Smart Collection based on the filter parameters provided. - - [:simple-plex: Plex Search](plex.md#plex-search) - Create a Collection based on items within the Plex library. + - [:simple-plex: Smart Filter](plex/smart-filter.md) - Create a Smart Collection based on the filter parameters provided. + - [:simple-plex: Plex Search](plex/search) - Create a Collection based on items within the Plex library. !!! builder @@ -88,7 +88,7 @@ Builders use third-party services to source items to be added to the collection. - [:material-television-guide: TVDb Show](tvdb.md#tvdb-show) - Grabs the specified series. - [:material-television-guide: TVDb Movie](tvdb.md#tvdb-movie) - Gets the specified movies. - - [:material-television-guide: TVDb List](tvdb.md#tvdb-list) - Gets every item in a TVDb List or TVDb UserList. + - [:material-television-guide: TVDb List](tvdb.md#tvdb-list) - Gets every item in a TVDb list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or TVDb UserList. !!! builder diff --git a/docs/files/builders/plex.md b/docs/files/builders/plex.md deleted file mode 100644 index 436862db4..000000000 --- a/docs/files/builders/plex.md +++ /dev/null @@ -1,428 +0,0 @@ ---- -hide: - - toc ---- - -# Plex Builders - -Plex Builders utilize media that already exists within your Plex library to place them into a Collection. - -Plex has two categories of Collections - Smart and Manual. The same search criteria can be used to build a Smart or Manual collection in most instances. - -To keep things simple we will use the term Smart Builders and Manual Builders, which refers to the criteria that is used to build the Collection. - -Smart Builders use filters (rules) to build collections that match the criteria of the Builder. When new media is added to your library, or if metadata of any item in your library changes to match the Builder's rules, the media is automatically included in the collection without the need to run Kometa again. - -Manual (also known as Dumb or non-Smart) Builders are static in nature and will not dynamically update as new media is added/metadata criteria changes across your library - you will have to run Kometa any time you want the Builder to re-run. - -Smart Builders are usually the recommended approach as they are lightweight and faster to process than Dumb Builders - -## Understanding Smart vs Manual Collections - -There are some key differences that you should understand before choosing if you want to use a Smart or Manual Builder, this table shows you the key differences between the Collections that are built from each type of Builder. - -| Feature | **Smart Collection** | **Manual (Dumb) Collection** | -|--------------------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| Automation | Automatically includes or removes items as your library changes | Requires manual updates—items must be added or removed by hand | -| Use Case | Great for auto-generating groups like “Movies Released This Year” or “Top-Rated Thrillers” | Ideal for curated lists like “My Favorites” or “Family Movie Night Picks” | -| Customization | Built using Plex’s filter system; limited to available filter options | Fully customizable—any item from the library can be included | -| Accuracy | Always reflects real-time library data based on filter criteria | Remains the same unless manually edited | -| Icon | Identified with a gear icon in the Plex UI (⚙️) | No special icon—just a standard collection | -| Behavior After Library Scan | Automatically refreshes based on updated metadata | No change unless edited manually | -| Editing | Filters can be edited to change which items are included | Items must be individually added/removed | -| Metadata Changes (via Refresh or Kometa updates) | Filters remain intact; collection contents may shift as metadata changes | Content stays fixed unless explicitly modified by scripts or user | - -There are some exceptions to the above when using a Smart Builder (namely when using a [Smart Label Definition](../settings.md#smart-label-definitions)), but the above should offer a basic understanding of the key differences. - -???+ important - - The `Smart Builders` and `Manual Builders` tabs below will give examples of how to use the Builders, and the `Search Options`, `Sort Options` and `Builder Attributes` tabs will give the full list of attributes and customizations available for use with the Builders. - -=== "Smart Builders" - - Smart Builders allow Kometa to create Smart Collections in two different ways. - - The results of these builders are dynamic and do not require Kometa to re-run in order to update, instead they will - update automatically as the data within your Plex Library updates (i.e. if new media is added) - - Smart Filter and Smart Label are the two methods available for Smart Builders. - - Smart Filter Bulders use Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) to create a smart collection based on the filter parameters provided. Any Advanced Filter made using the Plex UI should be able to be recreated using `smart_filter`. This is the normal approach used when your Builder criteria is held solely within Plex, and no third-party service involvement is required. - - ???+ important - - Smart Builders do not work with Playlists - - === "Smart Filter Builder" - - Like Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/), you have to start each filter with either `any` or `all` as a base. You can only - have one base attribute and all filter attributes must be under the base. - - Inside the base attribute you can use any filter below or nest more `any` or `all`. You can have as many nested `any` - or `all` next to each other as you want. If using multiple `any` or `all` you will have to do so in the form of a list. - - **Note: To search by `season`, `episode`, `album`, or `track` you must use the `builder_level` [Setting](../settings.md) - to change the type of items the collection holds.** - - - ### Example Smart Filter Builder(s) - - ```yaml - collections: - Documentaries: - smart_filter: - all: - genre: Documentary - ``` - ```yaml - collections: - Dave Chappelle Comedy: - smart_filter: - all: - actor: Dave Chappelle - genre: Comedy - ``` - ```yaml - collections: - Top Action Movies: - smart_filter: - all: - genre: Action - sort_by: audience_rating.desc - limit: 20 - ``` - ```yaml - collections: - 90s Movies: - smart_filter: - any: - year: - - 1990 - - 1991 - - 1992 - - 1993 - - 1994 - - 1995 - - 1996 - - 1997 - - 1998 - - 1999 - ``` - ```yaml - collections: - 90s Movies: - smart_filter: - any: - decade: 1990 - ``` - - If you specify TMDb Person ID's using the Setting `tmdb_person` and then tell either `actor`, `director`, `producer`, or - `writer` to add `tmdb`, the script will translate the TMDb Person IDs into their names and run the filter on those names. - - ```yaml - collections: - Robin Williams: - smart_filter: - all: - actor: tmdb - tmdb_person: 2157 - ``` - ```yaml - collections: - Steven Spielberg: - smart_filter: - all: - director: tmdb - tmdb_person: https://www.themoviedb.org/person/488-steven-spielberg - ``` - ```yaml - collections: - Quentin Tarantino: - smart_filter: - any: - actor: tmdb - director: tmdb - producer: tmdb - writer: tmdb - tmdb_person: 138 - ``` - - {% - include-markdown "../../templates/snippets/plex_search_options.md" - %} - - {% - include-markdown "../../templates/snippets/plex_sort_options.md" - %} - - {% - include-markdown "../../templates/snippets/plex_builder_attributes.md" - %} - - -=== "Manual Builders" - - This Builder finds items by using data held solely within Plex. - - The results of these builders are static and require Kometa to re-run in order to update. - - | Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | - |:----------------------------------------------|:----------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| - | [`plex_all`](#plex-all) | Gets every movie/show in your library. Useful with [Filters](../filters.md) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | [`plex_search`](#plex-search) | Gets every movie/show based on the search parameters provided | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | [`plex_watchlist`](#plex-watlist) | Gets every movie/show in your Watchlist. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | [`plex_pilots`](#plex-pilots) | Gets the first episode of every show in your library | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | [`plex_collectionless`](#plex-collectionless) | Gets every movie/show that is not in a collection | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - - === "Plex All" - - Finds every item in your library. Useful with [Filters](../filters.md). - - The expected input is either true or false. - - ### Example Plex All Builder(s) - - ```yaml - collections: - 9.0 Movies: - plex_all: true - filters: - user_rating.gte: 9 - ``` - - === "Plex Search" - - Uses Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) to find all items based on the search parameters provided. - - Any Advanced Filter made using the Plex UI should be able to be recreated using `plex_search`. If you're having trouble - getting `plex_search` to work correctly, build the collection you want inside of Plex's Advanced Filters and take a - screenshot of the parameters in the Plex UI and post it in either the - [Discussions](https://github.com/Kometa-Team/Kometa/discussions) or on [Discord](https://kometa.wiki/en/latest/discord/), - and I'll do my best to help you. - - like Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) you have to start each search with either `any` or `all` as a base. You can only - have one base attribute and all search attributes must be under the base. - - Inside the base attribute you can use any search below or nest more `any` or `all`. You can have as many nested `any` - or `all` next to each other as you want. If using multiple `any` or `all` you will have to do so in the form of a list. - - **Note: To search by `season`, `episode`, `album`, or `track` you must use the `builder_level` [Setting](../settings.md) to change - the type of items the collection holds.** - - There are a couple other attributes you can have at the top level only along with the base attribute are: - - - ### Example Plex Search Builder(s) - - ```yaml - collections: - Documentaries: - plex_search: - all: - genre: Documentary - ``` - ```yaml - collections: - Dave Chappelle Comedy: - plex_search: - all: - actor: Dave Chappelle - genre: Comedy - ``` - ```yaml - collections: - Top Action Movies: - collection_order: custom - plex_search: - all: - genre: Action - sort_by: audience_rating.desc - limit: 20 - ``` - ```yaml - collections: - 90s Movies: - plex_search: - any: - year: - - 1990 - - 1991 - - 1992 - - 1993 - - 1994 - - 1995 - - 1996 - - 1997 - - 1998 - - 1999 - ``` - ```yaml - collections: - 90s Movies: - plex_search: - any: - decade: 1990 - ``` - ```yaml - collections: - Best 2010+ Movies: - collection_order: custom - plex_search: - all: - year.gte: 2010 - sort_by: - - year.desc - - audience_rating.desc - limit: 20 - ``` - - Here's an example of an episode collection using `plex_search`. - - ```yaml - collections: - Top 100 Simpsons Episodes: - collection_order: custom - builder_level: episode - plex_search: - type: episode - sort_by: audience_rating.desc - limit: 100 - all: - title.ends: "Simpsons" - summary: A collection of the highest rated simpsons episodes. - ``` - - If you specify TMDb Person ID's using the Setting `tmdb_person` and then tell either `actor`, `director`, `producer`, or - `writer` to add `tmdb`, the script will translate the TMDb Person IDs into their names and run the search on those names. - - ```yaml - collections: - Robin Williams: - plex_search: - all: - actor: tmdb - tmdb_person: 2157 - ``` - ```yaml - collections: - Steven Spielberg: - plex_search: - all: - director: tmdb - tmdb_person: https://www.themoviedb.org/person/488-steven-spielberg - ``` - ```yaml - collections: - Quentin Tarantino: - plex_search: - any: - actor: tmdb - director: tmdb - producer: tmdb - writer: tmdb - tmdb_person: 138 - ``` - - === "Plex Watchlist" - - Finds every item in your Watchlist. - - The expected input is the sort you want returned. It defaults to `added.asc`. - - ### Watchlist Sort Options - - | Sort Option | Description | - |:--------------------------------------------|:--------------------------------------------| - | `title.asc`
`title.desc` | Sort by Title | - | `release.asc`
`release.desc` | Sort by Release Date (Originally Available) | - | `critic_rating.asc`
`critic_rating.desc` | Sort by Critic Rating | - | `added.asc`
`added.desc` | Sort by Date Added to your Watchlist | - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended. - - ### Example Plex Watchlist Builder(s) - - ```yaml - collections: - My Watchlist: - plex_watchlist: critic_rating.desc - collection_order: custom - sync_mode: sync - ``` - - === "Plex Pilots" - - Gets the first episode of every show in your library. This only works with `builder_level: episode` - - ### Example Plex Pilots Builder(s) - - ```yaml - collections: - Pilots: - builder_level: episode - plex_pilots: true - ``` - - === "Plex Collectionless" - - **This is not needed if you're using [Smart Label Collections](#smart-label).** - - Finds every item that is not in a collection unless the collection is in the exclusion list. This is a special - collection type to help keep your library looking correct. When items in your library are in multiple collections it - can mess up how they're displayed in your library. - - For Example, if you have a `Marvel Cinematic Universe` Collection set to `Show this collection and its items` and an - `Iron Man` Collection set to `Hide items in this collection` what happens is the show overrides the hide, and you end - up with both the collections and the 3 Iron Man movies all displaying. - - Alternatively, if you set the `Marvel Cinematic Universe` Collection to `Hide items in this collection` then movies - without a collection like `The Incredible Hulk` will be hidden from the library view. - - To combat the problem above you set all collections to `Hide items in this collection` then create a collection set to - `Hide collection` and put every movie that you still want to display in that collection. - - With the variability of collections generated by the Kometa maintaining a collection like this becomes very difficult, - so in order to automate it, you can use `plex_collectionless`. You just have to tell it what collections to exclude or - what collection prefixes to exclude. - - There are two attributes for `plex_collectionless`: - - * `exclude`: Exclude these Collections from being considered for collectionless. - * `exclude_prefix` Exclude Collections whose title or sort title starts with a prefix from being considered for - collectionless. - - **At least one exclusion is required.** - - ### Example Plex Collectionless Builder(s) - - ```yaml - collections: - Collectionless: - plex_collectionless: - exclude_prefix: - - "!" - - "+" - - "~" - exclude: - - Marvel Cinematic Universe - sort_title: ~_Collectionless - collection_order: alpha - collection_mode: hide - ``` - - * Both `exclude` and `exclude_prefix` can take multiple values as a List. - * This is a known issue with Plex Collection and there is a [Feature Suggestion](https://forums.plex.tv/t/collection-display-issue/305406) detailing the issue more on their - forms. - - - {% - include-markdown "../../templates/snippets/plex_search_options.md" - %} - - {% - include-markdown "../../templates/snippets/plex_sort_options.md" - %} - - {% - include-markdown "../../templates/snippets/plex_builder_attributes.md" - %} diff --git a/docs/files/builders/plex/all.md b/docs/files/builders/plex/all.md new file mode 100644 index 000000000..39113a117 --- /dev/null +++ b/docs/files/builders/plex/all.md @@ -0,0 +1,23 @@ +--- +hide: + - toc +--- +# Plex All + +Finds every item in your library. Useful with [Filters](../filters.md). + +The expected input is either true or false. + +{% + include-markdown "./sort-options.md" +%} + +### Example Plex All Builder(s) + +```yaml +collections: + 9.0 Movies: + plex_all: true + filters: + user_rating.gte: 9 +``` \ No newline at end of file diff --git a/docs/files/builders/plex/collectionless.md b/docs/files/builders/plex/collectionless.md new file mode 100644 index 000000000..811f327c7 --- /dev/null +++ b/docs/files/builders/plex/collectionless.md @@ -0,0 +1,54 @@ +--- +hide: + - toc +--- +# Plex Collectionless + +**This is not needed if you're using [Smart Label Collections](#smart-label).** + +Finds every item that is not in a collection unless the collection is in the exclusion list. This is a special +collection type to help keep your library looking correct. When items in your library are in multiple collections it +can mess up how they're displayed in your library. + +For Example, if you have a `Marvel Cinematic Universe` Collection set to `Show this collection and its items` and an +`Iron Man` Collection set to `Hide items in this collection` what happens is the show overrides the hide, and you end +up with both the collections and the 3 Iron Man movies all displaying. + +Alternatively, if you set the `Marvel Cinematic Universe` Collection to `Hide items in this collection` then movies +without a collection like `The Incredible Hulk` will be hidden from the library view. + +To combat the problem above you set all collections to `Hide items in this collection` then create a collection set to +`Hide collection` and put every movie that you still want to display in that collection. + +With the variability of collections generated by the Kometa maintaining a collection like this becomes very difficult, +so in order to automate it, you can use `plex_collectionless`. You just have to tell it what collections to exclude or +what collection prefixes to exclude. + +There are two attributes for `plex_collectionless`: + +* `exclude`: Exclude these Collections from being considered for collectionless. +* `exclude_prefix` Exclude Collections whose title or sort title starts with a prefix from being considered for +collectionless. + +**At least one exclusion is required.** + +### Example Plex Collectionless Builder(s) + +```yaml +collections: + Collectionless: + plex_collectionless: + exclude_prefix: + - "!" + - "+" + - "~" + exclude: + - Marvel Cinematic Universe + sort_title: ~_Collectionless + collection_order: alpha + collection_mode: hide +``` + +* Both `exclude` and `exclude_prefix` can take multiple values as a List. +* This is a known issue with Plex Collection and there is a [Feature Suggestion](https://forums.plex.tv/t/collection-display-issue/305406) detailing the issue more on their +forms. \ No newline at end of file diff --git a/docs/files/builders/plex/overview.md b/docs/files/builders/plex/overview.md new file mode 100644 index 000000000..6fe5966af --- /dev/null +++ b/docs/files/builders/plex/overview.md @@ -0,0 +1,67 @@ +--- +hide: + - toc +--- + +# Plex Builders + +Plex Builders utilize media that already exists within your Plex library to place them into a Collection. + +Plex has two categories of Collections - Smart and Manual. The same search criteria can be used to build a Smart or Manual collection in most instances. + +To keep things simple we will use the term Smart Builders and Manual Builders, which refers to the criteria that is used to build the Collection. + +Smart Builders use filters (rules) to build collections that match the criteria of the Builder. When new media is added to your library, or if metadata of any item in your library changes to match the Builder's rules, the media is automatically included in the collection without the need to run Kometa again. + +Manual (also known as Dumb or non-Smart) Builders are static in nature and will not dynamically update as new media is added/metadata criteria changes across your library - you will have to run Kometa any time you want the Builder to re-run. + +Smart Builders are usually the recommended approach as they are lightweight and faster to process than Dumb Builders + +## Understanding Smart vs Manual Collections + +There are some key differences that you should understand before choosing if you want to use a Smart or Manual Builder, this table shows you the key differences between the Collections that are built from each type of Builder. + +| Feature | **Smart Collection** | **Manual (Dumb) Collection** | +|--------------------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| Automation | Automatically includes or removes items as your library changes | Requires manual updates—items must be added or removed by hand | +| Use Case | Great for auto-generating groups like “Movies Released This Year” or “Top-Rated Thrillers” | Ideal for curated lists like “My Favorites” or “Family Movie Night Picks” | +| Customization | Built using Plex’s filter system; limited to available filter options | Fully customizable—any item from the library can be included | +| Accuracy | Always reflects real-time library data based on filter criteria | Remains the same unless manually edited | +| Icon | Identified with a gear icon in the Plex UI (⚙️) | No special icon—just a standard collection | +| Behavior After Library Scan | Automatically refreshes based on updated metadata | No change unless edited manually | +| Editing | Filters can be edited to change which items are included | Items must be individually added/removed | +| Metadata Changes (via Refresh or Kometa updates) | Filters remain intact; collection contents may shift as metadata changes | Content stays fixed unless explicitly modified by scripts or user | + +There are some exceptions to the above when using a Smart Builder (namely when using a [Smart Label Definition](../../settings.md#smart-label-definitions)), but the above should offer a basic understanding of the key differences. + +## Builder Attributes + +The majority of Smart and Manual Builders utilize the same Builder Attributes. Any deviation from this will be highlighted against the specific Builder. + +| Attribute | Description & Values | +|:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | **Description:** The max number of item for the filter.
**Default:** `all`
**Values:** `all` or a number greater than 0 | +| `sort_by` | **Description:** This will control how the filter is sorted in your library. You can do a multi-level sort using a list.
**Default:** `random`
**Values:** Any sort options for your filter type in the [Sorts Options Table](#sort-options) | +| `validate` | **Description:** Determines if a collection will fail on a validation error
**Default:** `true`
**Values**: `true` or `false` | + +## Plex Builder Types + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:--------------------------------------------------|:----------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`smart_filter`](smart-filter.md) | use Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) to create a smart collection based on the filter parameters provided. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`plex_all`](all.md) | Gets every movie/show in your library. Useful with [Filters](../../filters.md) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`plex_search`](search.md) | Gets every movie/show based on the search parameters provided | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`plex_watchlist`](watchlist.md) | Gets every movie/show in your Watchlist. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`plex_pilots`](pilots.md) | Gets the first episode of every show in your library | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`plex_collectionless`](collectionless.md) | Gets every movie/show that is not in a collection | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + + +{% + include-markdown "./search-options.md" +%} + +{% + include-markdown "./sort-options.md" +%} + + diff --git a/docs/files/builders/plex/pilots.md b/docs/files/builders/plex/pilots.md new file mode 100644 index 000000000..4acb42c07 --- /dev/null +++ b/docs/files/builders/plex/pilots.md @@ -0,0 +1,20 @@ +--- +hide: + - toc +--- +# Plex Pilots + +Gets the first episode of every show in your library. This only works with `builder_level: episode` + +{% + include-markdown "./sort-options.md" +%} + +### Example Plex Pilots Builder(s) + +```yaml +collections: + Pilots: + builder_level: episode + plex_pilots: true +``` \ No newline at end of file diff --git a/docs/files/builders/plex/search-options.md b/docs/files/builders/plex/search-options.md new file mode 100644 index 000000000..8c0356959 --- /dev/null +++ b/docs/files/builders/plex/search-options.md @@ -0,0 +1,206 @@ +## Plex Search Options + +When using a Plex Builder, there are three elements that correlate to Plex's Advanced Filters in the Web UI. + +- **Attribute:** What attribute you wish to filter. +- **Modifier:** Which modifier to use. +- **Value:** Actual value to filter. + +These three elements combined would look like `attribute.modifier: value`, in a Builder this may be something like `title.not: Harry Potter` or `episode_added: 7`. + +Attribute and Value elements are mandatory, whilst modifiers are optional. Typically speaking, Plex will have a default modifier of "is" or "contains" if you do not specify a modifier. + +The majority of Smart and Manual Builders utilize the same Search Options, meaning that the criteria for the builders is largely interchangeable between the two. Any deviation from this will be highlighted against the specific Builder. + +=== "Boolean Filters" + + Boolean Filters take no modifier and can only be either `true` or `false`. + + #### Boolean Filter Attributes + + | Boolean Search | Description | Movie
Libraries | Show
Libraries | Music
Libraries | + |:--------------------|:-----------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| + | `hdr` | Is HDR | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `unmatched` | Is Unmatched | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `duplicate` | Is Duplicate | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `unplayed` | Is Unplayed | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `progress` | Is In Progress | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `trash` | Is Trashed | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `unplayed_episodes` | Has Unplayed Episodes | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_unplayed` | Has Episodes Unplayed | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_duplicate` | Has Duplicate Episodes | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_progress` | Has Episode Progress | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_unmatched` | Has Episodes Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `show_unmatched` | Has Shows Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `artist_unmatched` | Is Artist's Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_unmatched` | Is Album's Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_trash` | Is Track Trashed | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `dovi` | Has Dolby Vision | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + +=== "Date Filters" + + Date filters can be used with either no modifier or with `.not`, `.before`, or `.after`. + + No date filter can take multiple values. + + + #### Date Filter Attributes + + | Date Search | Description | Movie
Libraries | Show
Libraries | Music
Libraries | + |:----------------------|:-----------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| + | `added` | Uses the date added attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_added` | Uses the date added attribute of the show's episodes to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `release` | Uses the release date attribute (originally available) to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_air_date` | Uses the air date attribute (originally available) of the show's episodes to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `last_played` | Uses the date last played attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_last_played` | Uses the date last played attribute of the show's episodes to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `artist_added` | Uses the Artist's date added attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_last_played` | Uses the Artist's last played attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_last_played` | Uses the Album's last played attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_added` | Uses the Album's date added attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_released` | Uses the Album's release date attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_last_played` | Uses the Track's date last played attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_last_skipped` | Uses the Track's date last skipped attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_last_rated` | Uses the Track's date last rated attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_added` | Uses the Track's date added attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + + ???+ tip "Date Filter Modifiers" + + | Date Modifier | Description | Plex Web UI Display | + |:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------:| + | No Modifier | Matches every item where the date attribute is in the last X days
**Format:** number of days
**Example:** `30` | `is in the last` | + | `.not` | Matches every item where the date attribute is not in the last X days
**Format:** number of days
**Example:** `30` | `is not in the last` | + | `.before` | Matches every item where the date attribute is before the given date
**Format:** MM/DD/YYYY or `today` for the current day
**Example:** `01/01/2000` | `is before` | + | `.after` | Matches every item where the date attribute is after the given date
**Format:** MM/DD/YYYY or `today` for the current day
**Example:** `01/01/2000` | `is after` | + +=== "Number Filters" + + Number filters must use `.gt`, `.gte`, `.lt`, or `.lte` as a modifier only the rating filters can use `.rated`. + + No number filter can take multiple values. + + #### Number Filter Attributes + + | Number Search | Description | Movie
Libraries | Show
Libraries | Music
Libraries | + |:---------------------------|:--------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| + | `duration` | Uses the duration attribute to match using minutes
**Minimum:** `0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `plays` | Uses the plays attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_plays` | Uses the Episode's plays attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `critic_rating` | Uses the critic rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `audience_rating` | Uses the audience rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `user_rating` | Uses the user rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_user_rating` | Uses the user rating attribute of the show's episodes to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the Episode's year attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `album_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the Album's year attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_decade` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the Album's decade attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_plays` | Uses the Album's plays attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_plays` | Uses the Track's plays attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_skips` | Uses the Track's skips attribute to match
**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_user_rating` | Uses the Artist's user rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_user_rating` | Uses the Album's user rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_critic_rating` | Uses the Album's critic rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_user_rating` | Uses the Track's user rating attribute to match
**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + + ???+ tip "Number Filter Modifiers" + + | Number Modifier | Description | Plex Web UI Display | + |:----------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------:| + | `.gt` | Matches every item where the number attribute is greater than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | `is greater than` | + | `.gte` | Matches every item where the number attribute is greater than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | N/A | + | `.lt` | Matches every item where the number attribute is less than the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | `is less than` | + | `.lte` | Matches every item where the number attribute is less than or equal to the given number
**Format:** number
**Example:** `30`, `1995`, or `7.5` | N/A | + | `.rated` | Matches every item either rated or not rated
**Format:** `true` or `false` | N/A | + + * `.rated` only works for rating filters + +=== "String Filters" + + String filters can be used with either no modifier or with `.not`, `.is`, `.isnot`, `.begins`, or `.ends`. + + String filter can take multiple values **only as a list**. + + #### String Filter Attributes + + | String Search | Description | Movie
Libraries | Show
Libraries | Music
Libraries | + |:---------------------|:---------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| + | `title` | Uses the title attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_title` | Uses the title attribute of the show's episodes to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `studio` | Uses the studio attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `edition` | Uses the edition attribute to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `artist_title` | Uses the Artist's Title attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_title` | Uses the Album's Title attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_title` | Uses the Track's Title attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_record_label` | Uses the Album's Record Label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + + ???+ tip "String Filter Modifiers" + + | String Modifier | Description | Plex Web UI Display | + |:----------------|:-------------------------------------------------------------------------------|:-------------------:| + | No Modifier | Matches every item where the attribute contains the given string | `contains` | + | `.not` | Matches every item where the attribute does not contain the given string | `does not contain` | + | `.is` | Matches every item where the attribute exactly matches the given string | `is` | + | `.isnot` | Matches every item where the attribute does not exactly match the given string | `is not` | + | `.begins` | Matches every item where the attribute begins with the given string | `begins with` | + | `.ends` | Matches every item where the attribute ends with the given string | `ends with` | + +=== "Tag Filters" + + Tag filters can be used with either no modifier or with `.not` except for `decade` and `resolution` which can only be + used with no modifier. + + Tag filter can take multiple values as a **list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string**. + + #### Tag Filter Attributes + + | Tag Search | Description | Movie
Libraries | Show
Libraries | Music
Libraries | + |:---------------------------|:----------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| + | `actor` | Uses the actor tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_actor` | Uses the episode actor tags to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `audio_language` | Uses the audio language tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `collection` | Uses the collection tags to match for top level collections | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `season_collection` | Uses the collection tags to match for season collections | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_collection` | Uses the collection tags to match for episode collections | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `content_rating` | Uses the content rating tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `country` | Uses the country tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `decade` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year tag to match the decade | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `director` | Uses the director tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `genre` | Uses the genre tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `label` | Uses the label tags to match for top level collections | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `season_label` | Uses the label tags to match for season collections | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_label` | Uses the label tags to match for episode collections | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `network` | Uses the network tags to match
**Only works with the New Plex TV Agent** | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `producer` | Uses the actor tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `resolution` | Uses the resolution tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `subtitle_language` | Uses the subtitle language tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `writer` | Uses the writer tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year tag to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the year tag to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `artist_genre` | Uses the Artist's Genre attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_collection` | Uses the Artist's Collection attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_country` | Uses the Artist's Country attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_mood` | Uses the Artist's Mood attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_style` | Uses the Artist's Style attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `artist_label` | Uses the Artist's Label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_genre` | Uses the Album's Genre attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_mood` | Uses the Album's Mood attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_style` | Uses the Album's Style attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_format` | Uses the Album's Format attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_type` | Uses the Album's Type attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_collection` | Uses the Album's Collection attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_source` | Uses the Album's Source attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_label` | Uses the Album's Label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_mood` | Uses the Track's Mood attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_source` | Uses the Track's Source attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `track_label` | Uses the Track's Label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + + :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }You can use `current_year` to have Kometa use the current years value. This can be combined with a + `-#` at the end to subtract that number of years. i.e. `current_year-2` + + ???+ tip "Tag Filter Modifiers" + + | Tag Modifier | Description | Plex Web UI Display | + |:-------------|:-----------------------------------------------------------------------|:-------------------:| + | No Modifier | Matches every item where the attribute matches the given string | `is` | + | `.not` | Matches every item where the attribute does not match the given string | `is not` | + diff --git a/docs/files/builders/plex/search.md b/docs/files/builders/plex/search.md new file mode 100644 index 000000000..5f9a5be8e --- /dev/null +++ b/docs/files/builders/plex/search.md @@ -0,0 +1,151 @@ +--- +hide: + - toc +--- +# Plex Search + +Uses Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) to find all items based on the search parameters provided. + +Any Advanced Filter made using the Plex UI should be able to be recreated using `plex_search`. If you're having trouble +getting `plex_search` to work correctly, build the collection you want inside of Plex's Advanced Filters and take a +screenshot of the parameters in the Plex UI and post it in either the +[Discussions](https://github.com/Kometa-Team/Kometa/discussions) or on [Discord](https://kometa.wiki/en/latest/discord/), +and I'll do my best to help you. + +like Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) you have to start each search with either `any` or `all` as a base. You can only +have one base attribute and all search attributes must be under the base. + +Inside the base attribute you can use any search below or nest more `any` or `all`. You can have as many nested `any` +or `all` next to each other as you want. If using multiple `any` or `all` you will have to do so in the form of a list. + +**Note: To search by `season`, `episode`, `album`, or `track` you must use the `builder_level` [Setting](../settings.md) to change +the type of items the collection holds.** + +{% + include-markdown "./search-options.md" +%} + +{% + include-markdown "./sort-options.md" +%} + +## Plex Builder Attributes + +The majority of Smart and Manual Builders utilize the same Builder Attributes. Any deviation from this will be highlighted against the specific Builder. + +| Attribute | Description & Values | +|:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | **Description:** The max number of item for the filter.
**Default:** `all`
**Values:** `all` or a number greater than 0 | +| `sort_by` | **Description:** This will control how the filter is sorted in your library. You can do a multi-level sort using a list.
**Default:** `random`
**Values:** Any sort options for your filter type in the [Sorts Options Table](#sort-options) | +| `validate` | **Description:** Determines if a collection will fail on a validation error
**Default:** `true`
**Values**: `true` or `false` | + +### Example Plex Search Builder(s) + +```yaml +collections: + Documentaries: + plex_search: + all: + genre: Documentary +``` +```yaml +collections: + Dave Chappelle Comedy: + plex_search: + all: + actor: Dave Chappelle + genre: Comedy +``` +```yaml +collections: + Top Action Movies: + collection_order: custom + plex_search: + all: + genre: Action + sort_by: audience_rating.desc + limit: 20 +``` +```yaml +collections: + 90s Movies: + plex_search: + any: + year: + - 1990 + - 1991 + - 1992 + - 1993 + - 1994 + - 1995 + - 1996 + - 1997 + - 1998 + - 1999 +``` +```yaml +collections: + 90s Movies: + plex_search: + any: + decade: 1990 +``` +```yaml +collections: + Best 2010+ Movies: + collection_order: custom + plex_search: + all: + year.gte: 2010 + sort_by: + - year.desc + - audience_rating.desc + limit: 20 +``` + +Here's an example of an episode collection using `plex_search`. + +```yaml + collections: + Top 100 Simpsons Episodes: + collection_order: custom + builder_level: episode + plex_search: + type: episode + sort_by: audience_rating.desc + limit: 100 + all: + title.ends: "Simpsons" + summary: A collection of the highest rated simpsons episodes. +``` + +If you specify TMDb Person ID's using the Setting `tmdb_person` and then tell either `actor`, `director`, `producer`, or +`writer` to add `tmdb`, the script will translate the TMDb Person IDs into their names and run the search on those names. + +```yaml +collections: + Robin Williams: + plex_search: + all: + actor: tmdb + tmdb_person: 2157 +``` +```yaml +collections: + Steven Spielberg: + plex_search: + all: + director: tmdb + tmdb_person: https://www.themoviedb.org/person/488-steven-spielberg +``` +```yaml +collections: + Quentin Tarantino: + plex_search: + any: + actor: tmdb + director: tmdb + producer: tmdb + writer: tmdb + tmdb_person: 138 +``` \ No newline at end of file diff --git a/docs/files/builders/plex/smart-filter.md b/docs/files/builders/plex/smart-filter.md new file mode 100644 index 000000000..44322b471 --- /dev/null +++ b/docs/files/builders/plex/smart-filter.md @@ -0,0 +1,122 @@ +--- +hide: + - toc +--- +# Plex Smart Filter + +Smart Filters allow Kometa to create Smart Collections. The results of this builder are dynamic and do not require Kometa to re-run in order to update, instead they will update automatically as the data within your Plex Library updates (i.e. if new media is added) + +Smart Filter Builders use Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/) to create a smart collection based on the filter parameters provided. Any Advanced Filter made using the Plex UI should be able to be recreated using `smart_filter`. This is the normal approach used when your Builder criteria is held solely within Plex, and no third-party service involvement is required. + +???+ important + + Smart Filters do not work with Playlists + +Like Plex's [Advanced Filters](https://support.plex.tv/articles/201273953-collections/), you have to start each filter with either `any` or `all` as a base. You can only +have one base attribute and all filter attributes must be under the base. + +Inside the base attribute you can use any filter below or nest more `any` or `all`. You can have as many nested `any` +or `all` next to each other as you want. If using multiple `any` or `all` you will have to do so in the form of a list. + +**Note: To search by `season`, `episode`, `album`, or `track` you must use the `builder_level` [Setting](../settings.md) +to change the type of items the collection holds.** + +{% + include-markdown "./search-options.md" +%} + +{% + include-markdown "./sort-options.md" +%} + +## Plex Builder Attributes + +The majority of Smart and Manual Builders utilize the same Builder Attributes. Any deviation from this will be highlighted against the specific Builder. + +| Attribute | Description & Values | +|:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | **Description:** The max number of item for the filter.
**Default:** `all`
**Values:** `all` or a number greater than 0 | +| `sort_by` | **Description:** This will control how the filter is sorted in your library. You can do a multi-level sort using a list.
**Default:** `random`
**Values:** Any sort options for your filter type in the [Sorts Options Table](#sort-options) | +| `validate` | **Description:** Determines if a collection will fail on a validation error
**Default:** `true`
**Values**: `true` or `false` | + +### Example Smart Filter Builder(s) + +```yaml +collections: + Documentaries: + smart_filter: + all: + genre: Documentary +``` +```yaml +collections: + Dave Chappelle Comedy: + smart_filter: + all: + actor: Dave Chappelle + genre: Comedy +``` +```yaml +collections: + Top Action Movies: + smart_filter: + all: + genre: Action + sort_by: audience_rating.desc + limit: 20 +``` +```yaml +collections: + 90s Movies: + smart_filter: + any: + year: + - 1990 + - 1991 + - 1992 + - 1993 + - 1994 + - 1995 + - 1996 + - 1997 + - 1998 + - 1999 +``` +```yaml +collections: + 90s Movies: + smart_filter: + any: + decade: 1990 +``` + +If you specify TMDb Person ID's using the Setting `tmdb_person` and then tell either `actor`, `director`, `producer`, or +`writer` to add `tmdb`, the script will translate the TMDb Person IDs into their names and run the filter on those names. + +```yaml +collections: + Robin Williams: + smart_filter: + all: + actor: tmdb + tmdb_person: 2157 +``` +```yaml +collections: + Steven Spielberg: + smart_filter: + all: + director: tmdb + tmdb_person: https://www.themoviedb.org/person/488-steven-spielberg +``` +```yaml +collections: + Quentin Tarantino: + smart_filter: + any: + actor: tmdb + director: tmdb + producer: tmdb + writer: tmdb + tmdb_person: 138 +``` diff --git a/docs/files/builders/plex/sort-options.md b/docs/files/builders/plex/sort-options.md new file mode 100644 index 000000000..a104bc2e5 --- /dev/null +++ b/docs/files/builders/plex/sort-options.md @@ -0,0 +1,54 @@ +## Plex Sort Options + +The majority of Smart and Manual Builders utilize the same Sort Options. Any deviation from this will be highlighted against the specific Builder. + +| Sort Option | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Tracks | +|:------------------------------------------------|:----------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| `title.asc`
`title.desc` | Sort by Title | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `season.asc`
`season.desc` | Sort by Season | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `show.asc`
`show.desc` | Sort by Show | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `album_artist.asc`
`album_artist.desc` | Sort by Album Artist | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `artist.asc`
`artist.desc` | Sort by Artist | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `album.asc`
`album.desc` | Sort by Album | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `year.asc`
`year.desc` | Sort by Year | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `release.asc`
`release.desc` | Sort by Release Date (Originally Available) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `episode_release.asc`
`episode_release.desc` | Sort by Episode Release Date (Originally Available) | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `critic_rating.asc`
`critic_rating.desc` | Sort by Critic Rating | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `audience_rating.asc`
`audience_rating.desc` | Sort by Audience Rating | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `user_rating.asc`
`user_rating.desc` | Sort by User Rating | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `content_rating.asc`
`content_rating.desc` | Sort by Content Rating | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `duration.asc`
`duration.desc` | Sort by Duration | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `progress.asc`
`progress.desc` | Sort by Progress | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `played.asc`
`played.desc` | Sort by Date Last Played | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `plays.asc`
`plays.desc` | Sort by Number of Plays | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `unplayed.asc`
`unplayed.desc` | Sort by Unplayed | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `episode_added.asc`
`episode_added.desc` | Sort by Last Episode Date Added | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `added.asc`
`added.desc` | Sort by Date Added | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `viewed.asc`
`viewed.desc` | Sort by Date Last Viewed | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `rated.asc`
`rated.desc` | Sort by Date Last Rated | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `popularity.asc`
`popularity.desc` | Sort by Popularity | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `resolution.asc`
`resolution.desc` | Sort by Resolution | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| `bitrate.asc`
`bitrate.desc` | Sort by Bitrate | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `random` | Sort by Random | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + +### Sort Option Examples + +```yaml +collections: + Best 2024 Movies: + smart_filter: + limit: 100 + all: + year: 2024 + sort_by: audience_rating.desc +``` + +```yaml +collections: + Newly Released Movies: + smart_filter: + limit: 100 + all: + release: 30 + sort_by: release.desc +``` diff --git a/docs/files/builders/plex/watchlist.md b/docs/files/builders/plex/watchlist.md new file mode 100644 index 000000000..e4cf94342 --- /dev/null +++ b/docs/files/builders/plex/watchlist.md @@ -0,0 +1,30 @@ +--- +hide: + - toc +--- +# Plex Watchlist + +Finds every item in your Watchlist. + +The expected input is the sort you want returned. It defaults to `added.asc`. + +## Watchlist Sort Options + +| Sort Option | Description | +|:--------------------------------------------|:--------------------------------------------| +| `title.asc`
`title.desc` | Sort by Title | +| `release.asc`
`release.desc` | Sort by Release Date (Originally Available) | +| `critic_rating.asc`
`critic_rating.desc` | Sort by Critic Rating | +| `added.asc`
`added.desc` | Sort by Date Added to your Watchlist | + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended. + +### Example Plex Watchlist Builder(s) + +```yaml +collections: + My Watchlist: + plex_watchlist: critic_rating.desc + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/radarr.md b/docs/files/builders/radarr.md deleted file mode 100644 index cc2f46f98..000000000 --- a/docs/files/builders/radarr.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -hide: - - toc ---- -# Radarr Builders - -You can find items in your Plex using the features of [Radarr](https://radarr.video/). - -???+ warning "Radarr Configuration" - - [Configuring Radarr](../../config/radarr.md) in the config is required for any of these builders. - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:------------------------------------|:---------------------------------------------|:------------------------------------------:|:----------------------------------------:|:----------------------------------------:| -| [`radarr_all`](#radarr-all) | Gets all Movies in Radarr. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| [`radarr_taglist`](#radarr-taglist) | Gets Movies from Radarr based on their tags. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | - -=== "Radarr All" - - Gets all Movies in Radarr. - - ### Example Radarr All Builder(s) - - ```yaml - collections: - ALL Radarr Movies: - radarr_all: true - ``` - -=== "Radarr Taglist" - - Gets Movies from Radarr based on their tags. - - Set the attribute to the tag you want to search for. Multiple values are supported as either a list or a comma-separated string. - - ### Example Radarr Taglist Builder(s) - - ```yaml - collections: - Radarr Tag1 and Tag2 Movies: - radarr_taglist: tag1, tag2 - ``` - - If no tag is specified then it gets every Movie without a tag. - - ```yaml - collections: - Radarr Movies Without Tags: - radarr_taglist: - ``` \ No newline at end of file diff --git a/docs/files/builders/radarr/all.md b/docs/files/builders/radarr/all.md new file mode 100644 index 000000000..79d610be2 --- /dev/null +++ b/docs/files/builders/radarr/all.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# Radarr All + +Gets all Movies in Radarr. + +???+ warning "Radarr Configuration" + + [Configuring Radarr](../../../config/radarr.md) in the config is required for any of these builders. + +### Example Radarr All Builder(s) + +```yaml +collections: + ALL Radarr Movies: + radarr_all: true +``` diff --git a/docs/files/builders/radarr/overview.md b/docs/files/builders/radarr/overview.md new file mode 100644 index 000000000..010c9a519 --- /dev/null +++ b/docs/files/builders/radarr/overview.md @@ -0,0 +1,16 @@ +--- +hide: + - toc +--- +# Radarr Builders + +You can find items in your Plex using the features of [Radarr](https://radarr.video/). + +???+ warning "Radarr Configuration" + + [Configuring Radarr](../../../config/radarr.md) in the config is required for any of these builders. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------------|:---------------------------------------------|:------------------------------------------:|:----------------------------------------:|:----------------------------------------:| +| [`radarr_all`](all.md) | Gets all Movies in Radarr. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| [`radarr_taglist`](taglist.md) | Gets Movies from Radarr based on their tags. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/files/builders/radarr/taglist.md b/docs/files/builders/radarr/taglist.md new file mode 100644 index 000000000..e4621fb50 --- /dev/null +++ b/docs/files/builders/radarr/taglist.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# Radarr Taglist + +Gets Movies from Radarr based on their tags. + +Set the attribute to the tag you want to search for. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +???+ warning "Radarr Configuration" + + [Configuring Radarr](../../../config/radarr.md) in the config is required for any of these builders. + +### Example Radarr Taglist Builder(s) + +```yaml +collections: + Radarr Tag1 and Tag2 Movies: + radarr_taglist: tag1, tag2 +``` + +If no tag is specified then it gets every Movie without a tag. + +```yaml +collections: + Radarr Movies Without Tags: + radarr_taglist: +``` \ No newline at end of file diff --git a/docs/files/builders/reciperr.md b/docs/files/builders/reciperr.md deleted file mode 100644 index cd18e3fd0..000000000 --- a/docs/files/builders/reciperr.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -hide: - - toc ---- -# Reciperr Builders - -You can find movies using a Reciperr list on [reciperr.com](https://reciperr.com/) (Reciperr). - -No configuration is required for this Builder. - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:----------------------------------|:-----------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| -| [`reciperr_list`](#reciperr-list) | Finds every movie at a Reciperr JSON data URL. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - -## Reciperr List - -Finds every movie on Reciperr a list. - -The expected input is the url that points to the JSON data or a list of urls that do. - -The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. - -### Example Reciperr List Builder(s) - -```yaml -collections: - Reciperr Movies: - reciperr_list: https://reciperr.com/api/recipe/list/params?recipeMetadataId=62354f0e89a919001d650fa3 - collection_order: custom - sync_mode: sync -``` diff --git a/docs/files/builders/reciperr/list.md b/docs/files/builders/reciperr/list.md new file mode 100644 index 000000000..123ad6333 --- /dev/null +++ b/docs/files/builders/reciperr/list.md @@ -0,0 +1,21 @@ +--- +hide: + - toc +--- +# Reciperr List + +Finds every movie on Reciperr a list. + +The expected input is the url that points to the JSON data or a list of urls that do. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated and in a specific order. + +### Example Reciperr List Builder(s) + +```yaml +collections: + Reciperr Movies: + reciperr_list: https://reciperr.com/api/recipe/list/params?recipeMetadataId=62354f0e89a919001d650fa3 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/reciperr/overview.md b/docs/files/builders/reciperr/overview.md new file mode 100644 index 000000000..4e61e2402 --- /dev/null +++ b/docs/files/builders/reciperr/overview.md @@ -0,0 +1,13 @@ +--- +hide: + - toc +--- +# Reciperr Builders + +You can find movies using a Reciperr list on [reciperr.com](https://reciperr.com/) (Reciperr). + +No configuration is required for this Builder. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:---------------------------|:-----------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| +| [`reciperr_list`](list.md) | Finds every movie at a Reciperr JSON data URL. | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | \ No newline at end of file diff --git a/docs/files/builders/sonarr.md b/docs/files/builders/sonarr.md deleted file mode 100644 index a9ec334ce..000000000 --- a/docs/files/builders/sonarr.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -hide: - - toc ---- -# Sonarr Builders - -You can find items in your Plex using the features of [Sonarr](https://sonarr.tv/). - -???+ warning "Sonarr Configuration" - - [Configuring Sonarr](../../config/sonarr.md) in the config is required for any of these builders. - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:------------------------------------|:---------------------------------------------|:----------------------------------------:|:------------------------------------------:|:----------------------------------------:| -| [`sonarr_all`](#sonarr-all) | Gets all Series in Sonarr. | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`sonarr_taglist`](#sonarr-taglist) | Gets Series from Sonarr based on their tags. | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - -=== "Sonarr All" - - Gets all Series in Sonarr. - - ### Example Sonarr All Builder(s) - - ```yaml - collections: - ALL Sonarr Series: - sonarr_all: true - ``` - -=== "Sonarr Taglist" - - Gets Series from Sonarr based on their tags. - - Set the attribute to the tag you want to search for. Multiple values are supported as either a list or a comma-separated - string. - - ### Example Sonarr Taglist Builder(s) - - ```yaml - collections: - Sonarr Tag1 and Tag2 Series: - sonarr_taglist: tag1, tag2 - ``` - - If no tag is specified then it gets every Movie without a tag. - - ```yaml - collections: - Sonarr Series Without Tags: - sonarr_taglist: - ``` \ No newline at end of file diff --git a/docs/files/builders/sonarr/all.md b/docs/files/builders/sonarr/all.md new file mode 100644 index 000000000..f174054a0 --- /dev/null +++ b/docs/files/builders/sonarr/all.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# Sonarr All + +Gets all Series in Sonarr. + +???+ warning "Sonarr Configuration" + + [Configuring Sonarr](../../../config/sonarr.md) in the config is required for any of these builders. + +### Example Sonarr All Builder(s) + +```yaml +collections: + ALL Sonarr Series: + sonarr_all: true +``` diff --git a/docs/files/builders/sonarr/overview.md b/docs/files/builders/sonarr/overview.md new file mode 100644 index 000000000..316635ddb --- /dev/null +++ b/docs/files/builders/sonarr/overview.md @@ -0,0 +1,16 @@ +--- +hide: + - toc +--- +# Sonarr Builders + +You can find items in your Plex using the features of [Sonarr](https://sonarr.tv/). + +???+ warning "Sonarr Configuration" + + [Configuring Sonarr](../../../config/sonarr.md) in the config is required for any of these builders. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------------|:---------------------------------------------|:----------------------------------------:|:------------------------------------------:|:----------------------------------------:| +| [`sonarr_all`](all.md) | Gets all Series in Sonarr. | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`sonarr_taglist`](taglist.md) | Gets Series from Sonarr based on their tags. | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/files/builders/sonarr/taglist.md b/docs/files/builders/sonarr/taglist.md new file mode 100644 index 000000000..14d6bb3dd --- /dev/null +++ b/docs/files/builders/sonarr/taglist.md @@ -0,0 +1,30 @@ +--- +hide: + - toc +--- +# Sonarr Taglist + +Gets Series from Sonarr based on their tags. + +Set the attribute to the tag you want to search for. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated +string. + +???+ warning "Sonarr Configuration" + + [Configuring Sonarr](../../../config/sonarr.md) in the config is required for any of these builders. + +### Example Sonarr Taglist Builder(s) + +```yaml +collections: + Sonarr Tag1 and Tag2 Series: + sonarr_taglist: tag1, tag2 +``` + +If no tag is specified then it gets every Movie without a tag. + +```yaml +collections: + Sonarr Series Without Tags: + sonarr_taglist: +``` \ No newline at end of file diff --git a/docs/files/builders/stevenlu.md b/docs/files/builders/stevenlu.md deleted file mode 100644 index 5c346a86b..000000000 --- a/docs/files/builders/stevenlu.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -hide: - - toc ---- -# StevenLu Builders - -You can find items using StevenLu's Popular Movies list on [StevenLu.com](https://movies.stevenlu.com/) (StevenLu). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:-----------------------------------------------------|:-------------------------------------------------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| -| [`stevenlu_popular`](#stevenlus-popular-movies-list) | Finds every movie on [StevenLu's Popular Movies List](https://movies.stevenlu.com/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - -## StevenLu's Popular Movies List - -Finds every movie on [StevenLu's Popular Movies List](https://movies.stevenlu.com/). - -The expected input is `true`. - -The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated -and in a specific order. - -### Example StevenLu's Popular Movies List Builder(s) - -```yaml -collections: - StevenLu's Popular Movies: - stevenlu_popular: true - collection_order: custom - sync_mode: sync -``` diff --git a/docs/files/builders/stevenlu/overview.md b/docs/files/builders/stevenlu/overview.md new file mode 100644 index 000000000..f714e0cb3 --- /dev/null +++ b/docs/files/builders/stevenlu/overview.md @@ -0,0 +1,11 @@ +--- +hide: + - toc +--- +# StevenLu Builders + +You can find items using StevenLu's Popular Movies list on [StevenLu.com](https://movies.stevenlu.com/) (StevenLu). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:---------------------------------|:-------------------------------------------------------------------------------------|:------------------------------------------:|:----------------------------------------:|:------------------------------------------:| +| [`stevenlu_popular`](popular.md) | Finds every movie on [StevenLu's Popular Movies List](https://movies.stevenlu.com/). | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | \ No newline at end of file diff --git a/docs/files/builders/stevenlu/popular.md b/docs/files/builders/stevenlu/popular.md new file mode 100644 index 000000000..f63a379c7 --- /dev/null +++ b/docs/files/builders/stevenlu/popular.md @@ -0,0 +1,22 @@ +--- +hide: + - toc +--- +# StevenLu's Popular Movies List + +Finds every movie on [StevenLu's Popular Movies List](https://movies.stevenlu.com/). + +The expected input is `true`. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example StevenLu's Popular Movies List Builder(s) + +```yaml +collections: + StevenLu's Popular Movies: + stevenlu_popular: true + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tautulli.md b/docs/files/builders/tautulli.md deleted file mode 100644 index e8fe93a6a..000000000 --- a/docs/files/builders/tautulli.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -hide: - - toc ---- -# Tautulli Builders - -You can find items in your Plex using the features of [Tautulli](https://tautulli.com/). - -It has watch analytics that can show the most watched or most popular Movies/Shows in each Library. - -???+ warning "Tautulli Configuration" - - [Configuring Tautulli](../../config/tautulli.md) in the config is required for any of these builders. - - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:-----------------------------------------------|:------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`tautulli_popular`](#tautulli-popularwatched) | Gets the Tautulli Most Popular List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tautulli_watched`](#tautulli-popularwatched) | Gets the Tautulli Most Watched List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - -Both Tautulli Popular and Tautulli Watched have the same sub-attributes detailed below. - -## Tautulli Popular/Watched - -| Attribute | Description | Required | Default | -|:---------------|:-----------------------------------------------------------|:----------------------------------------:|:-------:| -| `list_days` | Number of Days to look back of the list. | :fontawesome-solid-circle-xmark:{ .red } | `30` | -| `list_minimum` | Minimum Number of Users Watching/Plays to add to the list. | :fontawesome-solid-circle-xmark:{ .red } | `0` | -| `list_size` | Number of Movies/Shows to add to this list. | :fontawesome-solid-circle-xmark:{ .red } | `10` | - -The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated -and in a specific order. - -### Example Tautulli Popular/Watched Builder(s) - -```yaml -collections: - Most Popular Movies (30 Days): - sync_mode: sync - collection_order: custom - tautulli_popular: - list_days: 30 - list_size: 10 -``` -```yaml -collections: - Most Watched Movies (30 Days): - sync_mode: sync - collection_order: custom - tautulli_watched: - list_days: 30 - list_size: 10 -``` -```yaml -collections: - Plex Popular: - tautulli_popular: - list_days: 30 - list_size: 20 - tautulli_watched: - list_days: 30 - list_size: 20 - sync_mode: sync - summary: Movies Popular on Plex - collection_order: alpha -``` -```yaml -playlists: - Plex Popular: - libraries: Movies - tautulli_popular: - list_days: 30 - list_size: 20 - sync_mode: sync - summary: Movies Popular on Plex -``` diff --git a/docs/files/builders/tautulli/overview.md b/docs/files/builders/tautulli/overview.md new file mode 100644 index 000000000..006492b4c --- /dev/null +++ b/docs/files/builders/tautulli/overview.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# Tautulli Builders + +You can find items in your Plex using the features of [Tautulli](https://tautulli.com/). + +It has watch analytics that can show the most watched or most popular Movies/Shows in each Library. + +???+ warning "Tautulli Configuration" + + [Configuring Tautulli](../../../config/tautulli.md) in the config is required for any of these builders. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:---------------------------------|:------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`tautulli_popular`](popular.md) | Gets the Tautulli Most Popular List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tautulli_watched`](watched.md) | Gets the Tautulli Most Watched List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + diff --git a/docs/files/builders/tautulli/popular.md b/docs/files/builders/tautulli/popular.md new file mode 100644 index 000000000..daba59336 --- /dev/null +++ b/docs/files/builders/tautulli/popular.md @@ -0,0 +1,53 @@ +--- +hide: + - toc +--- +# Tautulli Popular + +| Attribute | Description | Required | Default | +|:---------------|:-----------------------------------------------------------|:----------------------------------------:|:-------:| +| `list_days` | Number of Days to look back of the list. | :fontawesome-solid-circle-xmark:{ .red } | `30` | +| `list_minimum` | Minimum Number of Users Watching/Plays to add to the list. | :fontawesome-solid-circle-xmark:{ .red } | `0` | +| `list_size` | Number of Movies/Shows to add to this list. | :fontawesome-solid-circle-xmark:{ .red } | `10` | + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ warning "Tautulli Configuration" + + [Configuring Tautulli](../../../config/tautulli.md) in the config is required for any of these builders. + +### Example Tautulli Popular Builder(s) + +```yaml +collections: + Most Popular Movies (30 Days): + sync_mode: sync + collection_order: custom + tautulli_popular: + list_days: 30 + list_size: 10 +``` +```yaml +collections: + Plex Popular: + tautulli_popular: + list_days: 30 + list_size: 20 + tautulli_watched: + list_days: 30 + list_size: 20 + sync_mode: sync + summary: Movies Popular on Plex + collection_order: alpha +``` +```yaml +playlists: + Plex Popular: + libraries: Movies + tautulli_popular: + list_days: 30 + list_size: 20 + sync_mode: sync + summary: Movies Popular on Plex +``` diff --git a/docs/files/builders/tautulli/watched.md b/docs/files/builders/tautulli/watched.md new file mode 100644 index 000000000..d70cffb4d --- /dev/null +++ b/docs/files/builders/tautulli/watched.md @@ -0,0 +1,44 @@ +--- +hide: + - toc +--- +# Tautulli Watched + +| Attribute | Description | Required | Default | +|:---------------|:-----------------------------------------------------------|:----------------------------------------:|:-------:| +| `list_days` | Number of Days to look back of the list. | :fontawesome-solid-circle-xmark:{ .red } | `30` | +| `list_minimum` | Minimum Number of Users Watching/Plays to add to the list. | :fontawesome-solid-circle-xmark:{ .red } | `0` | +| `list_size` | Number of Movies/Shows to add to this list. | :fontawesome-solid-circle-xmark:{ .red } | `10` | + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ warning "Tautulli Configuration" + + [Configuring Tautulli](../../../config/tautulli.md) in the config is required for any of these builders. + +### Example Tautulli Watched Builder(s) + +```yaml +collections: + Most Watched Movies (30 Days): + sync_mode: sync + collection_order: custom + tautulli_watched: + list_days: 30 + list_size: 10 +``` +```yaml +collections: + Plex Popular: + tautulli_popular: + list_days: 30 + list_size: 20 + tautulli_watched: + list_days: 30 + list_size: 20 + sync_mode: sync + summary: Movies Popular on Plex + collection_order: alpha +``` + diff --git a/docs/files/builders/tmdb.md b/docs/files/builders/tmdb.md deleted file mode 100644 index ccf42a8c5..000000000 --- a/docs/files/builders/tmdb.md +++ /dev/null @@ -1,744 +0,0 @@ ---- -hide: - - toc ---- -# TMDb Builders - -You can find items using the features of [TheMovieDb.org](https://www.themoviedb.org/) (TMDb). - -## TMDb Standard Builders - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:---------------------------------|:---------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`tmdb_discover`](#discover) | Uses [TMDb's Discover Search](https://developer.themoviedb.org/docs/search-and-query-for-details) to find every movie/show based on the [movie search parameters](https://developers.themoviedb.org/3/discover/movie-discover) or [show search parameters](https://developers.themoviedb.org/3/discover/tv-discover) provided | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_collection`](#collection) | Finds every item in the TMDb collection | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_list`](#list) | Finds every item in the TMDb List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_movie`](#movie) | Finds the movie specified | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_show`](#show) | Finds the show specified | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_company`](#company) | Finds every item from the TMDb company's movie/show list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_network`](#network) | Finds every item from the TMDb network's show list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_keyword`](#keyword) | Finds every item from the TMDb keyword's movie/show list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - -## TMDb Chart Builders - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:-------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------:|:------------------------------------------:|:-------------------------------------------:| -| [`tmdb_popular`](#popular) | Finds the movies/shows in TMDb's [Popular Movies](https://www.themoviedb.org/movie)/[Popular Shows](https://www.themoviedb.org/tv) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_now_playing`](#now-playing) | Finds the movies in TMDb's [Now Playing](https://www.themoviedb.org/movie/now-playing) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_top_rated`](#top-rated) | Finds the movies/shows in TMDb's [Top Rated Movies](https://www.themoviedb.org/movie/top-rated)/[Top Rated Shows](https://www.themoviedb.org/tv/top-rated) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_upcoming`](#upcoming) | Finds the movies in TMDb's [Upcoming Movies](https://www.themoviedb.org/movie/upcoming) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_airing_today`](#airing-today) | Finds the shows in TMDb's [Airing Today Shows](https://www.themoviedb.org/tv/airing-today) list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_on_the_air`](#on-the-air) | Finds the shows in TMDb's [On TV Shows](https://www.themoviedb.org/tv/on-the-air) list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_trending_daily`](#trending-daily) | Finds the movies/shows in TMDb's Trending Daily list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tmdb_trending_weekly`](#trending-weekly) | Finds the movies/shows in TMDb's Trending Weekly list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - - -## TMDb People Builders - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:-----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------:|:------------------------------------------:|:-------------------------------------------:| -| [`tmdb_actor`](#actor) | Finds every item in the TMDb Person's Actor Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_crew`](#crew) | Finds every item in the TMDb Person's Crew Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_director`](#director) | Finds every item in the TMDb Person's Director Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_producer`](#producer) | Finds every item in the TMDb Person's Producer Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tmdb_writer`](#writer) | Finds every item in the TMDb Person's Writer Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - -=== "TMDb Standard Builders" - - These are the most commonly used TMDb Builders. - - === "Discover" - - Uses [TMDb's Discover Search](https://developer.themoviedb.org/docs/search-and-query-for-details) to find every - movie/show based on the [movie search attributes](https://developers.themoviedb.org/3/discover/movie-discover) or - [show search attributes](https://developers.themoviedb.org/3/discover/tv-discover) provided. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - | Type | Description | - |:-------------------|:--------------------------------------------------| - | String | Any number of alphanumeric characters | - | Integer | Any whole number greater than zero i.e. 2, 10, 50 | - | Number | Any number greater than zero i.e. 2.5, 7.4, 9 | - | Boolean | Must be `true` or `false` | - | Date: `MM/DD/YYYY` | Date that fits the specified format | - | Year: `YYYY` | Year must be a 4 digit integer i.e. 1990 | - - === "Discover Movies Attributes" - - !!!important - - Note that a number of filters support being comma (,) or pipe (|) separated. Comma's are treated like an AND query - while pipe's are treated like an OR. This allows for quite complex filtering depending on your desired results. - - !!!bug - - We have noticed inconsistent responses from TMDb when using `popularity.asc` and `popularity.desc` as the sort order. This can result in movies/shows disappearing from and reapparing in collections/overlays sporadically. **We suggest users do not use the popularity sort options with `tmdb_discover`**. - - This bug is on TMDb's side and we are awaiting a fix from them. - - | Attribute | Description | - |:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `limit` | Specify how many movies you want returned by the query.
**Type:** Integer
**Default:** 100 | - | `region` | Specify a [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) to filter release dates. Must be uppercase. Will use the `region` specified in the [TMDb Config](../../config/tmdb.md) by default.
**Type:** `^[A-Z]{2}$` | - | `sort_by` | Choose from one of the many available sort options.
**Type:** Any sort option below
**Default:** `popularity.desc` | - | `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.
**Type:** String | - | `certification` | Filter results with a valid certification from the `certification_country` parameter.
**Type:** String | - | `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.
**Type:** String | - | `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.
**Type:** String | - | `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | - | `include_video` | A filter and include or exclude videos.
**Type:** Boolean | - | `primary_release_year` | A filter to limit the results to a specific primary release year.
**Type:** Year: YYYY | - | `primary_release_date.gte` | Filter and only include movies that have a primary release date that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `primary_release_date.lte` | Filter and only include movies that have a primary release date that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `release_date.gte` | Filter and only include movies that have a release date (looking at all release dates) that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `release_date.lte` | Filter and only include movies that have a release date (looking at all release dates) that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.
**Type:** String
**Values:** `1`: Premiere, `2`: Theatrical (limited), `3`: Theatrical, `4`: Digital, `5`: Physical, `6`: TV | - | `year` | A filter to limit the results to a specific year (looking at all release dates).
**Type:** Year: `YYYY` | - | `vote_count.gte` | Filter and only include movies that have a vote count that is greater or equal to the specified value.
**Type:** Integer | - | `vote_count.lte` | Filter and only include movies that have a vote count that is less than or equal to the specified value.
**Type:** Integer | - | `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.
**Type:** Number | - | `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.
**Type:** Number | - | `with_cast` | A comma-separated list of person ID's. Only include movies that have one of the ID's added as an actor.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `with_crew` | A comma-separated list of person ID's. Only include movies that have one of the ID's added as a crew member.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `with_people` | A comma-separated list of person ID's. Only include movies that have one of the ID's added as either an actor or a crew member.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `with_companies` | A comma-separated list of production company ID's. Only include movies that have one of the ID's added as a production company.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | - | `with_genres` | Comma-separated value of genre ids that you want to include in the results.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**Type:** String | - | `with_keywords` | A comma-separated list of keyword ID's. Only includes movies that have one of the ID's added as a keyword.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | - | `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.
**Type:** Integer | - | `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.
**Type:** Integer | - | `with_origin_country` | Specify an origin country string to filter results by their original country value.
**Type:** String | - | `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | - | `with_watch_providers` | A comma or pipe separated list of watch provider ID's.
use in conjunction with watch_region, can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_watch_providers` | Filter the results to exclude certain watch providers.
**Type:** String | - | `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.
**Type:** String
**Values:** [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | - | `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | - - === "Discover Shows Attributes" - - ???+ warning "Important" - - Note that a number of filters support being comma (,) or pipe (|) separated. Comma's are treated like an AND query - while pipe's are treated like an OR. This allows for quite complex filtering depending on your desired results. - - - | Attributes | Description | - |:--------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `limit` | Specify how many movies you want to be returned by the query.
**Type:** Integer
**Default:** 100 | - | `sort_by` | Choose from one of the many available sort options.
**Type:** Any sort option below
**Default:** `popularity.desc` | - | `air_date.gte` | Filter and only include TV shows that have an air date (by looking at all episodes) that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `air_date.lte` | Filter and only include TV shows that have an air date (by looking at all episodes) that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | - | `first_air_date.gte` | Filter and only include TV shows that have a original air date that is greater or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Date: `MM/DD/YYYY` | - | `first_air_date.lte` | Filter and only include TV shows that have a original air date that is less than or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Date: `MM/DD/YYYY` | - | `first_air_date_year` | Filter and only include TV shows that have an original air date year that equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Year: `YYYY` | - | `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | - | `include_null_first_air_dates` | Use this filter to include TV shows that don't have an air date while using any of the `first_air_date` filters.
**Type:** Boolean | - | `timezone` | Used in conjunction with the `air_date.gte/lte` filter to calculate the proper UTC offset.
**Type:** String
**Default:** `America/New_York` | - | `vote_count.gte` | Filter and only include TV that have a vote count that is greater or equal to the specified value.
**Type:** Integer | - | `vote_count.lte` | Filter and only include TV that have a vote count that is less than or equal to the specified value.
**Type:** Integer | - | `vote_average.gte` | Filter and only include TV that have a rating that is greater or equal to the specified value.
**Type:** Number | - | `vote_average.lte` | Filter and only include TV that have a rating that is less than or equal to the specified value.
**Type:** Number | - | `with_networks` | Comma-separated value of network ids that you want to include in the results.
**Type:** String | - | `with_companies` | A comma-separated list of production company ID's. Only include movies that have one of the ID's added as a production company.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | - | `with_genres` | Comma-separated value of genre ids that you want to include in the results.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**Type:** String | - | `with_keywords` | A comma-separated list of keyword ID's. Only includes TV shows that have one of the ID's added as a keyword.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | - | `with_runtime.gte` | Filter and only include TV shows with an episode runtime that is greater than or equal to a value.
**Type:** Integer | - | `with_runtime.lte` | Filter and only include TV shows with an episode runtime that is less than or equal to a value.
**Type:** Integer | - | `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | - | `with_name_translation` | Specify a language/country string to filter the results by if the item has a type of name translation.
**Type:** String
**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` | - | `screened_theatrically` | Filter results to include items that have been screened theatrically.
**Type:** Boolean | - | `with_watch_providers` | A comma or pipe separated list of watch provider ID's.
use in conjunction with watch_region, can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | - | `without_watch_providers` | Filter the results to exclude certain watch providers.
**Type:** String | - | `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.
**Type:** String | - | `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | - | `with_status` | Filter TV shows by their status.
**Type:** String
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Values:** `0`: Returning Series, `1`: Planned, `2`: In Production, `3`: Ended, `4`: Cancelled, `5`: Pilot | - | `with_type` | Filter TV shows by their type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `0`: Documentary, `1`: News, `2`: Miniseries, `3`: Reality, `4`: Scripted, `5`: Show, `6`: Video | - - === "Sort Options" - - | Sort Option | Movie Sort | Show Sort | - |:----------------------------|:------------------------------------------:|:------------------------------------------:| - | `popularity.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | `popularity.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | `title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `original_title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `original_title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `original_name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `original_name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `revenue.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `revenue.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `primary_release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `primary_release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `first_air_date.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `first_air_date.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `vote_average.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | `vote_average.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | `vote_count.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - | `vote_count.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | - - ### Example TMDb Discover Builder(s) - - ```yaml - collections: - Movies Released in October 2020: - tmdb_discover: - primary_release_date.gte: 10/01/2020 - primary_release_date.lte: 10/31/2020 - ``` - ```yaml - collections: - Popular Movies: - collection_order: custom - sync_mode: sync - tmdb_discover: - sort_by: popularity.desc - ``` - ```yaml - collections: - Highest Rated R Movies: - collection_order: custom - sync_mode: sync - tmdb_discover: - certification_country: US - certification: R - sort_by: vote_average.desc - ``` - ```yaml - collections: - Most Popular Kids Movies: - collection_order: custom - sync_mode: sync - tmdb_discover: - certification_country: US - certification.lte: G - sort_by: popularity.desc - ``` - ```yaml - collections: - Highest Rated Movies From 2010: - collection_order: custom - sync_mode: sync - tmdb_discover: - primary_release_year: 2010 - sort_by: vote_average.desc - ``` - ```yaml - collections: - Best Dramas From 2014: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_genres: 18 - primary_release_year: 2014 - sort_by: vote_average.desc - ``` - ```yaml - collections: - Highest Rated Science Fiction Movies with Tom Cruise: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_genres: 878 - with_cast: 500 - sort_by: vote_average.desc - ``` - ```yaml - collections: - Highest Grossing Comedy Movies with Will Ferrell: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_genres: 35 - with_cast: 23659 - sort_by: revenue.desc - ``` - ```yaml - collections: - Top Rated Movies with Brad Pitt and Edward Norton: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_people: 287,819 - sort_by: vote_average.desc - ``` - ```yaml - collections: - Popular Movies with David Fincher and Rooney Mara: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_people: 108916,7467 - sort_by: popularity.desc - ``` - ```yaml - collections: - Top Rated Dramas: - collection_order: custom - sync_mode: sync - tmdb_discover: - with_genres: 18 - sort_by: vote_average.desc - vote_count.gte: 10 - ``` - ```yaml - collections: - Highest Grossing R Movies with Liam Neeson: - collection_order: custom - sync_mode: sync - tmdb_discover: - certification_country: US - certification: R - sort_by: revenue.desc - with_cast: 3896 - ``` - - - === "Collection" - - Finds every item in the TMDb collection. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - ### Example TMDb Collection Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - The Lord of the Rings: - tmdb_collection: - - 121938 #(3)! - The Hobbit: - tmdb_collection: https://www.themoviedb.org/collection/119 - Middle Earth: - tmdb_collection_details: #(1)! - - 119 #(2)! - - https://www.themoviedb.org/collection/121938 - ``` - - 1. You can replace `tmdb_collection` with `tmdb_collection_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list. - 2. You can specify multiple collections in `tmdb_collection_details` but it will only use the first one to update the collection details. - 3. https://www.themoviedb.org/collection/121938-the-hobbit-collection also accepted - - * Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. - - === "List" - - Finds every item in the TMDb List. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb List Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Top 50 Grossing Films of All Time (Worldwide): - tmdb_list: 10 #(1)! - collection_order: custom - sync_mode: sync - Marvel & DC Universes: - tmdb_list_details: #(2)! - - 1 #(3)! - - 3 - collection_order: custom - sync_mode: sync - ``` - - 1. https://www.themoviedb.org/list/10 also accepted - 2. You can replace `tmdb_list` with `tmdb_list_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list - 3. You can specify multiple lists in `tmdb_list_details` but it will only use the first one to update the collection details - - === "Movie" - - Finds the movie specified. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - ### Example TMDb Movie Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Anaconda: - tmdb_movie: #(1)! - - 336560 - Wizard of Oz & Wicked: - tmdb_movie_details: #(2)! - - 630 #(3)! - - 402431 - ``` - - 1. You can replace `tmdb_movie` with `tmdb_movie_details` if you would like to fetch and use the TMDb show's summary, poster, and background from the list - 2. You can specify multiple shows in `tmdb_movie_details` but it will only use the first one to update the collection details - 3. https://www.themoviedb.org/movie/630-the-wizard-of-oz also accepted - - * Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. - - - === "Show" - - Finds the show specified. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - ### Example TMDb Show Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Star Wars (Animated Shows): - tmdb_show: - - 4194 #(1)! - - 60554 - Pokémon Evolutions & Chronicles: - tmdb_show_details: #(2)! - - 132636 #(3)! - - 13230 - ``` - - 1. https://www.themoviedb.org/tv/4194-star-wars-the-clone-wars also accepted - 2. You can replace `tmdb_show` with `tmdb_show_details` if you would like to fetch and use the TMDb show's summary, poster, and background from the list - 3. You can specify multiple shows in `tmdb_show_details` but it will only use the first one to update the collection details - - * Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. - - === "Company" - - Finds every movie from the TMDb company's movie list. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - ### Example TMDb Company Builder(s) - - ```yaml - collections: - Studio Ghibli: - tmdb_company: 10342 #(1)! - ``` - - 1. https://www.themoviedb.org/company/10342 also accepted - - === "Network" - - Finds every item from the TMDb network's movie/show list. - - This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list or a comma-separated string. - - ### Example TMDb Network Builder(s) - - ```yaml - collections: - CBS: - tmdb_network: 16 #(1)! - ``` - - 1. https://www.themoviedb.org/network/16 also accepted - - - - === "Keyword" - - Finds every item from the TMDb keyword's movie/show list. - - ### Example TMDb Keyword Builder(s) - - ```yaml - collections: - Marvel Cinematic Universe: - tmdb_keyword: 180547 #(1)! - ``` - - 1. https://www.themoviedb.org/keyword/180547 also accepted - -=== "TMDb Chart Builders" - - These Builders use TMDb's Chart data - - === "Popular" - - Finds the movies/shows in TMDb's [Popular Movies](https://www.themoviedb.org/movie)/[Popular Shows](https://www.themoviedb.org/tv) list. - - Use `tmdb_region` with this Builder to set the region. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Popular Builder(s) - - ```yaml - collections: - TMDb Popular: - tmdb_popular: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Now Playing" - - Finds the movies in TMDb's [Now Playing](https://www.themoviedb.org/movie/now-playing) list. - - Use `tmdb_region` with this Builder to set the region. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Now Playing Builder(s) - - ```yaml - collections: - TMDb Now Playing: - tmdb_now_playing: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Top Rated" - - Finds the movies/shows in TMDb's [Top Rated Movies](https://www.themoviedb.org/movie/top-rated)/[Top Rated Shows](https://www.themoviedb.org/tv/top-rated) list. - - Use `tmdb_region` with this Builder to set the region. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Top Rated Builder(s) - - ```yaml - collections: - TMDb Top Rated: - tmdb_top_rated: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Upcoming" - - Finds the movies in TMDb's [Upcoming Movies](https://www.themoviedb.org/movie/upcoming) list. - - Use `tmdb_region` with this Builder to set the region. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Upcoming Builder(s) - - ```yaml - collections: - TMDb Upcoming: - tmdb_upcoming: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Airing Today" - - Finds the shows in TMDb's [Airing Today Shows](https://www.themoviedb.org/tv/airing-today) list. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Airing Today Builder(s) - - ```yaml - collections: - TMDb Airing Today: - tmdb_airing_today: 30 - collection_order: custom - sync_mode: sync - ``` - - === "On the Air" - - Finds the shows in TMDb's [On TV Shows](https://www.themoviedb.org/tv/on-the-air) list. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb On the Air Builder(s) - - ```yaml - collections: - TMDb On the Air: - tmdb_on_the_air: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Trending Daily" - - Finds the movies/shows in TMDb's Trending Daily list. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Trending Daily Builder(s) - - ```yaml - collections: - TMDb Daily Trending: - tmdb_trending_daily: 30 - collection_order: custom - sync_mode: sync - ``` - - === "Trending Weekly" - - Finds the movies/shows in TMDb's Trending Weekly list. - - This Builder is expected to have an integer (number) value of how many items to query - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example TMDb Trending Weekly Builder(s) - - ```yaml - collections: - TMDb Weekly Trending: - tmdb_trending_weekly: 30 - collection_order: custom - sync_mode: sync - ``` - -=== "TMDb People Builders" - - These Builders use data on people's credited work. - - ???+ tip "People Collections" - - As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. - - You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. - - === "Actor" - - Finds every item in the TMDb Person's Actor Credits. - - ### Example TMDb Actor Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Robin Williams: - tmdb_actor: 2157 #(1)! - Hemsworth Brothers: - tmdb_actor_details: #(2)! - - 74568 #(3)! - - 96066 - - 216986 - ``` - - 1. https://www.themoviedb.org/person/2157-robin-williams also accepted - 2. You can replace `tmdb_actor` with `tmdb_actor_details` if you would like to fetch and use the TMDb Person's biography and profile from the list - 3. You can specify multiple people in `tmdb_actor_details` but it will only use the first one to update the collection details - - === "Crew" - - Finds every item in the TMDb Person's Crew Credits. - - ### Example TMDb Crew Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Quentin Tarantino: - tmdb_crew: 138 #(1)! - The Skarsgards Family - tmdb_crew_details: #(2)! - - 28846 #(3)! - - 137905 - - 63764 - - 1640 - - 1281937 - - 2367741 - ``` - - 1. https://www.themoviedb.org/person/138-quentin-tarantino also accepted - 2. You can replace `tmdb_crew` with `tmdb_crew_details` if you would like to fetch and use the TMDb Person's biography and profile from the list - 3. You can specify multiple people in `tmdb_crew_details` but it will only use the first one to update the collection details - - === "Director" - - Finds every item in the TMDb Person's Director Credits. - - ### Example TMDb Director Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Steven Spielberg: - tmdb_director: 488 #(1)! - The Russo Brothers: - tmdb_director_details: #(2)! - - 19271 #(3)! - - 19272 - ``` - - 1. #https://www.themoviedb.org/person/488-steven-spielberg also accepted - 1. You can replace `tmdb_director` with `tmdb_director_details` if you would like to fetch and use the TMDb Person's biography and profile from the list - 3. You can specify multiple people in `tmdb_director_details` but it will only use the first one to update the collection details - - - === "Producer" - - Finds every item in the TMDb Person's Producer Credits. - - ### Example TMDb Producer Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Adam Sandler: - tmdb_producer: 19292 #(3)! - The Wachowskis: - tmdb_producer_details: #(1)! - - 9339 #(2)! - - 9340 - ``` - - 1. You can replace `tmdb_producer` with `tmdb_producer_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list - 2. You can specify multiple producers in `tmdb_producer_details` but it will only use the first one to update the collection details - 3. https://www.themoviedb.org/person/19292-adam-sandler also accepted - - === "Writer" - - Finds every item in the TMDb Person's Writer Credits. - - ### Example TMDb Writer Builder(s) - - ```yaml title="Press the + icon to learn more" - collections: - Woody Allen: - tmdb_writer: - - 1243 #(3)! - The Daniels:: - tmdb_writer_details: #(1)! - - 1383612 #(2)! - - https://www.themoviedb.org/person/1317730 - ``` - - 1. You can replace `tmdb_writer` with `tmdb_writer_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list - 2. You can specify multiple people in `tmdb_writer_details` but it will only use the first one to update the collection details - 3. https://www.themoviedb.org/person/1243-woody-allen also accepted - diff --git a/docs/files/builders/tmdb/actor.md b/docs/files/builders/tmdb/actor.md new file mode 100644 index 000000000..99050be27 --- /dev/null +++ b/docs/files/builders/tmdb/actor.md @@ -0,0 +1,30 @@ +--- +hide: + - toc +--- +# TMDb Actor + +???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +Finds every item in the TMDb Person's Actor Credits. + +### Example TMDb Actor Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Robin Williams: + tmdb_actor: 2157 #(1)! + Hemsworth Brothers: + tmdb_actor_details: #(2)! + - 74568 #(3)! + - 96066 + - 216986 +``` + +1. https://www.themoviedb.org/person/2157-robin-williams also accepted +2. You can replace `tmdb_actor` with `tmdb_actor_details` if you would like to fetch and use the TMDb Person's biography and profile from the list +3. You can specify multiple people in `tmdb_actor_details` but it will only use the first one to update the collection details diff --git a/docs/files/builders/tmdb/airing-today.md b/docs/files/builders/tmdb/airing-today.md new file mode 100644 index 000000000..bbc262b64 --- /dev/null +++ b/docs/files/builders/tmdb/airing-today.md @@ -0,0 +1,22 @@ +--- +hide: + - toc +--- +# TMDb Airing Today + +Finds the shows in TMDb's [Airing Today Shows](https://www.themoviedb.org/tv/airing-today) list. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Airing Today Builder(s) + +```yaml +collections: + TMDb Airing Today: + tmdb_airing_today: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/collection.md b/docs/files/builders/tmdb/collection.md new file mode 100644 index 000000000..43a491f73 --- /dev/null +++ b/docs/files/builders/tmdb/collection.md @@ -0,0 +1,31 @@ +--- +hide: + - toc +--- +# TMDb Collection + +Finds every item in the TMDb collection. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example TMDb Collection Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + The Lord of the Rings: + tmdb_collection: + - 121938 #(3)! + The Hobbit: + tmdb_collection: https://www.themoviedb.org/collection/119 + Middle Earth: + tmdb_collection_details: #(1)! + - 119 #(2)! + - https://www.themoviedb.org/collection/121938 +``` + +1. You can replace `tmdb_collection` with `tmdb_collection_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list. +2. You can specify multiple collections in `tmdb_collection_details` but it will only use the first one to update the collection details. +3. https://www.themoviedb.org/collection/121938-the-hobbit-collection also accepted + +* Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. + diff --git a/docs/files/builders/tmdb/company.md b/docs/files/builders/tmdb/company.md new file mode 100644 index 000000000..c3c4d13a2 --- /dev/null +++ b/docs/files/builders/tmdb/company.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# TMDb Company + +Finds every movie from the TMDb company's movie list. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example TMDb Company Builder(s) + +```yaml +collections: + Studio Ghibli: + tmdb_company: 10342 #(1)! +``` + +1. https://www.themoviedb.org/company/10342 also accepted diff --git a/docs/files/builders/tmdb/crew.md b/docs/files/builders/tmdb/crew.md new file mode 100644 index 000000000..03648c07b --- /dev/null +++ b/docs/files/builders/tmdb/crew.md @@ -0,0 +1,33 @@ +--- +hide: + - toc +--- +# TMDb Crew + +???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +Finds every item in the TMDb Person's Crew Credits. + +### Example TMDb Crew Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Quentin Tarantino: + tmdb_crew: 138 #(1)! + The Skarsgards Family: + tmdb_crew_details: #(2)! + - 28846 #(3)! + - 137905 + - 63764 + - 1640 + - 1281937 + - 2367741 +``` + +1. https://www.themoviedb.org/person/138-quentin-tarantino also accepted +2. You can replace `tmdb_crew` with `tmdb_crew_details` if you would like to fetch and use the TMDb Person's biography and profile from the list +3. You can specify multiple people in `tmdb_crew_details` but it will only use the first one to update the collection details diff --git a/docs/files/builders/tmdb/director.md b/docs/files/builders/tmdb/director.md new file mode 100644 index 000000000..918b1a2c2 --- /dev/null +++ b/docs/files/builders/tmdb/director.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# TMDb Director + +???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +Finds every item in the TMDb Person's Director Credits. + +### Example TMDb Director Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Steven Spielberg: + tmdb_director: 488 #(1)! + The Russo Brothers: + tmdb_director_details: #(2)! + - 19271 #(3)! + - 19272 +``` + +1. https://www.themoviedb.org/person/488-steven-spielberg also accepted +1. You can replace `tmdb_director` with `tmdb_director_details` if you would like to fetch and use the TMDb Person's biography and profile from the list +3. You can specify multiple people in `tmdb_director_details` but it will only use the first one to update the collection details diff --git a/docs/files/builders/tmdb/discover/movie.md b/docs/files/builders/tmdb/discover/movie.md new file mode 100644 index 000000000..1c5713dd6 --- /dev/null +++ b/docs/files/builders/tmdb/discover/movie.md @@ -0,0 +1,215 @@ +--- +hide: + - toc +--- +# TMDb Discover (Movies) + +Uses [TMDb's Discover Search](https://developer.themoviedb.org/docs/search-and-query-for-details) to find every movie based on the [movie search attributes](https://developers.themoviedb.org/3/discover/movie-discover) provided. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ important + + Note that a number of filters support being comma (,) or pipe (|) separated. Commas are treated like an AND query + while pipes are treated like an OR. This allows for quite complex filtering depending on your desired results. + +## Value Types + +These are the types of values you can use in your TMDb Discover queries. The type and formatting of attributes such as date and year is important to know when building your queries. + +| Type | Description | +|:-------------------|:--------------------------------------------------| +| String | Any number of alphanumeric characters | +| Integer | Any whole number greater than zero i.e. 2, 10, 50 | +| Number | Any number greater than zero i.e. 2.5, 7.4, 9 | +| Boolean | Must be `true` or `false` | +| Date: `MM/DD/YYYY` | Date that fits the specified format | +| Year: `YYYY` | Year must be a 4 digit integer i.e. 1990 | + +## Discover Attributes + +| Attribute | Description | +|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | Specify how many movies you want returned by the query.
**Type:** Integer
**Default:** 100 | +| `region` | Specify a [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) to filter release dates. Must be uppercase. Will use the `region` specified in the [TMDb Config](../../config/tmdb.md) by default.
**Type:** `^[A-Z]{2}$` | +| `sort_by` | Choose from one of the many available sort options.
**Type:** Any sort option below
**Default:** `popularity.desc` | +| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.
**Type:** String | +| `certification` | Filter results with a valid certification from the `certification_country` parameter.
**Type:** String | +| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.
**Type:** String | +| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.
**Type:** String | +| `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | +| `include_video` | A filter and include or exclude videos.
**Type:** Boolean | +| `primary_release_year` | A filter to limit the results to a specific primary release year.
**Type:** Year: YYYY | +| `primary_release_date.gte` | Filter and only include movies that have a primary release date that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `primary_release_date.lte` | Filter and only include movies that have a primary release date that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `release_date.gte` | Filter and only include movies that have a release date (looking at all release dates) that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `release_date.lte` | Filter and only include movies that have a release date (looking at all release dates) that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.
**Type:** String
**Values:** `1`: Premiere, `2`: Theatrical (limited), `3`: Theatrical, `4`: Digital, `5`: Physical, `6`: TV | +| `year` | A filter to limit the results to a specific year (looking at all release dates).
**Type:** Year: `YYYY` | +| `vote_count.gte` | Filter and only include movies that have a vote count that is greater or equal to the specified value.
**Type:** Integer | +| `vote_count.lte` | Filter and only include movies that have a vote count that is less than or equal to the specified value.
**Type:** Integer | +| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.
**Type:** Number | +| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.
**Type:** Number | +| `with_cast` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of person ID's. Only include movies that have one of the ID's added as an actor.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `with_crew` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of person ID's. Only include movies that have one of the ID's added as a crew member.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `with_people` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of person ID's. Only include movies that have one of the ID's added as either an actor or a crew member.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `with_companies` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of production company ID's. Only include movies that have one of the ID's added as a production company.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | +| `with_genres` | Comma-separated value of genre ids that you want to include in the results.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**Type:** String | +| `with_keywords` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keyword ID's. Only includes movies that have one of the ID's added as a keyword.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | +| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.
**Type:** Integer | +| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.
**Type:** Integer | +| `with_origin_country` | Specify an origin country string to filter results by their original country value.
**Type:** String | +| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | +| `with_watch_providers` | A comma or pipe separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of watch provider ID's.
use in conjunction with watch_region, can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_watch_providers` | Filter the results to exclude certain watch providers.
**Type:** String | +| `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.
**Type:** String
**Values:** [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) | +| `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | + + +## Sort Options + +| Sort Option | Movie Sort | Show Sort | +|:----------------------------|:------------------------------------------:|:------------------------------------------:| +| `popularity.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `popularity.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `original_title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `original_title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `original_name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `original_name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `revenue.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `revenue.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `primary_release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `primary_release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `first_air_date.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `first_air_date.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `vote_average.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_average.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_count.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_count.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + +### Example TMDb Discover Builder(s) + +```yaml +collections: + Movies Released in October 2020: + tmdb_discover: + primary_release_date.gte: 10/01/2020 + primary_release_date.lte: 10/31/2020 +``` +```yaml +collections: + Popular Movies: + collection_order: custom + sync_mode: sync + tmdb_discover: + sort_by: popularity.desc +``` +```yaml +collections: + Highest Rated R Movies: + collection_order: custom + sync_mode: sync + tmdb_discover: + certification_country: US + certification: R + sort_by: vote_average.desc +``` +```yaml +collections: + Most Popular Kids Movies: + collection_order: custom + sync_mode: sync + tmdb_discover: + certification_country: US + certification.lte: G + sort_by: popularity.desc +``` +```yaml +collections: + Highest Rated Movies From 2010: + collection_order: custom + sync_mode: sync + tmdb_discover: + primary_release_year: 2010 + sort_by: vote_average.desc +``` +```yaml +collections: + Best Dramas From 2014: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 18 + primary_release_year: 2014 + sort_by: vote_average.desc +``` +```yaml +collections: + Highest Rated Science Fiction Movies with Tom Cruise: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 878 + with_cast: 500 + sort_by: vote_average.desc +``` +```yaml +collections: + Highest Grossing Comedy Movies with Will Ferrell: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 35 + with_cast: 23659 + sort_by: revenue.desc +``` +```yaml +collections: + Top Rated Movies with Brad Pitt and Edward Norton: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_people: 287,819 + sort_by: vote_average.desc +``` +```yaml +collections: + Popular Movies with David Fincher and Rooney Mara: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_people: 108916,7467 + sort_by: popularity.desc +``` +```yaml +collections: + Top Rated Dramas: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 18 + sort_by: vote_average.desc + vote_count.gte: 10 +``` +```yaml +collections: + Highest Grossing R Movies with Liam Neeson: + collection_order: custom + sync_mode: sync + tmdb_discover: + certification_country: US + certification: R + sort_by: revenue.desc + with_cast: 3896 +``` + diff --git a/docs/files/builders/tmdb/discover/show.md b/docs/files/builders/tmdb/discover/show.md new file mode 100644 index 000000000..adf8b48dc --- /dev/null +++ b/docs/files/builders/tmdb/discover/show.md @@ -0,0 +1,162 @@ +--- +hide: + - toc +--- +# TMDb Discover (Shows) + +Uses [TMDb's Discover Search](https://developer.themoviedb.org/docs/search-and-query-for-details) to find every show based on the [show search attributes](https://developers.themoviedb.org/3/discover/show-discover) provided. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ important + + Note that a number of filters support being comma (,) or pipe (|) separated. Commas are treated like an AND query + while pipes are treated like an OR. This allows for quite complex filtering depending on your desired results. + +## Value Types + +These are the types of values you can use in your TMDb Discover queries. The type and formatting of attributes such as date and year is important to know when building your queries. + +| Type | Description | +|:-------------------|:--------------------------------------------------| +| String | Any number of alphanumeric characters | +| Integer | Any whole number greater than zero i.e. 2, 10, 50 | +| Number | Any number greater than zero i.e. 2.5, 7.4, 9 | +| Boolean | Must be `true` or `false` | +| Date: `MM/DD/YYYY` | Date that fits the specified format | +| Year: `YYYY` | Year must be a 4 digit integer i.e. 1990 | + +## Discover Attributes + +| Attributes | Description | +|:--------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | Specify how many movies you want to be returned by the query.
**Type:** Integer
**Default:** 100 | +| `sort_by` | Choose from one of the many available sort options.
**Type:** Any sort option below
**Default:** `popularity.desc` | +| `air_date.gte` | Filter and only include TV shows that have an air date (by looking at all episodes) that is greater or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `air_date.lte` | Filter and only include TV shows that have an air date (by looking at all episodes) that is less than or equal to the specified value.
**Type:** Date: `MM/DD/YYYY` | +| `first_air_date.gte` | Filter and only include TV shows that have a original air date that is greater or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Date: `MM/DD/YYYY` | +| `first_air_date.lte` | Filter and only include TV shows that have a original air date that is less than or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Date: `MM/DD/YYYY` | +| `first_air_date_year` | Filter and only include TV shows that have an original air date year that equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date.
**Type:** Year: `YYYY` | +| `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | +| `include_null_first_air_dates` | Use this filter to include TV shows that don't have an air date while using any of the `first_air_date` filters.
**Type:** Boolean | +| `timezone` | Used in conjunction with the `air_date.gte/lte` filter to calculate the proper UTC offset.
**Type:** String
**Default:** `America/New_York` | +| `vote_count.gte` | Filter and only include TV that have a vote count that is greater or equal to the specified value.
**Type:** Integer | +| `vote_count.lte` | Filter and only include TV that have a vote count that is less than or equal to the specified value.
**Type:** Integer | +| `vote_average.gte` | Filter and only include TV that have a rating that is greater or equal to the specified value.
**Type:** Number | +| `vote_average.lte` | Filter and only include TV that have a rating that is less than or equal to the specified value.
**Type:** Number | +| `with_networks` | Comma-separated value of network ids that you want to include in the results.
**Type:** String | +| `with_companies` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of production company ID's. Only include movies that have one of the ID's added as a production company.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | +| `with_genres` | Comma-separated value of genre ids that you want to include in the results.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**Type:** String | +| `with_keywords` | A comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keyword ID's. Only includes TV shows that have one of the ID's added as a keyword.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | +| `with_runtime.gte` | Filter and only include TV shows with an episode runtime that is greater than or equal to a value.
**Type:** Integer | +| `with_runtime.lte` | Filter and only include TV shows with an episode runtime that is less than or equal to a value.
**Type:** Integer | +| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | +| `with_name_translation` | Specify a language/country string to filter the results by if the item has a type of name translation.
**Type:** String
**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` | +| `screened_theatrically` | Filter results to include items that have been screened theatrically.
**Type:** Boolean | +| `with_watch_providers` | A comma or pipe separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of watch provider ID's.
use in conjunction with watch_region, can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String | +| `without_watch_providers` | Filter the results to exclude certain watch providers.
**Type:** String | +| `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.
**Type:** String | +| `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | +| `with_status` | Filter TV shows by their status.
**Type:** String
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Values:** `0`: Returning Series, `1`: Planned, `2`: In Production, `3`: Ended, `4`: Cancelled, `5`: Pilot | +| `with_type` | Filter TV shows by their type.
Can be a comma (`,`) for an AND, or a pipe (`|`) for an OR separated query
**Type:** String
**Values:** `0`: Documentary, `1`: News, `2`: Miniseries, `3`: Reality, `4`: Scripted, `5`: Show, `6`: Video | + + +## Sort Options + +| Sort Option | Movie Sort | Show Sort | +|:----------------------------|:------------------------------------------:|:------------------------------------------:| +| `popularity.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `popularity.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `original_title.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `original_title.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `original_name.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `original_name.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `revenue.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `revenue.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `primary_release_date.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `primary_release_date.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| `first_air_date.asc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `first_air_date.desc` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| `vote_average.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_average.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_count.asc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| `vote_count.desc` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + +### Example TMDb Discover Builder(s) + +```yaml +collections: + Shows Released in October 2020: + tmdb_discover: + first_air_date.gte: 10/01/2020 + first_air_date.lte: 10/31/2020 +``` +```yaml +collections: + 100 Most Popular Shows: + collection_order: custom + sync_mode: sync + tmdb_discover: + sort_by: popularity.desc + limit: 100 +``` +```yaml +collections: + Highest Rated TV-MA Shows: + collection_order: custom + sync_mode: sync + tmdb_discover: + certification_country: US + certification: TV-MA + sort_by: vote_average.desc +``` +```yaml +collections: + Most Popular Kids Shows: + collection_order: custom + sync_mode: sync + tmdb_discover: + certification_country: US + certification.lte: TV-G + sort_by: popularity.desc +``` +```yaml +collections: + Highest Rated Science Fiction Movies with Tom Cruise: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 878 + with_cast: 500 + sort_by: vote_average.desc +``` +```yaml +collections: + Streaming on Disney+ in UK: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_watch_providers: 337 + region: GB +``` +```yaml +collections: + Top Rated Dramas: + collection_order: custom + sync_mode: sync + tmdb_discover: + with_genres: 18 + sort_by: vote_average.desc + vote_count.gte: 10 +``` + diff --git a/docs/files/builders/tmdb/keyword.md b/docs/files/builders/tmdb/keyword.md new file mode 100644 index 000000000..017836a76 --- /dev/null +++ b/docs/files/builders/tmdb/keyword.md @@ -0,0 +1,17 @@ +--- +hide: + - toc +--- +# TMDb Keyword + +Finds every item from the TMDb keyword's movie/show list. + +### Example TMDb Keyword Builder(s) + +```yaml +collections: + Marvel Cinematic Universe: + tmdb_keyword: 180547 #(1)! +``` + +1. https://www.themoviedb.org/keyword/180547 also accepted diff --git a/docs/files/builders/tmdb/list.md b/docs/files/builders/tmdb/list.md new file mode 100644 index 000000000..e17c4554f --- /dev/null +++ b/docs/files/builders/tmdb/list.md @@ -0,0 +1,32 @@ +--- +hide: + - toc +--- +# TMDb List + +Finds every item in the TMDb List. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb List Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Top 50 Grossing Films of All Time (Worldwide): + tmdb_list: 10 #(1)! + collection_order: custom + sync_mode: sync + Marvel & DC Universes: + tmdb_list_details: #(2)! + - 1 #(3)! + - 3 + collection_order: custom + sync_mode: sync +``` + +1. https://www.themoviedb.org/list/10 also accepted +2. You can replace `tmdb_list` with `tmdb_list_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list +3. You can specify multiple lists in `tmdb_list_details` but it will only use the first one to update the collection details diff --git a/docs/files/builders/tmdb/movie.md b/docs/files/builders/tmdb/movie.md new file mode 100644 index 000000000..546c53dac --- /dev/null +++ b/docs/files/builders/tmdb/movie.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# TMDb Movie + +Finds the movie specified. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example TMDb Movie Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Anaconda: + tmdb_movie: #(1)! + - 336560 + Wizard of Oz & Wicked: + tmdb_movie_details: #(2)! + - 630 #(3)! + - 402431 +``` + +1. You can replace `tmdb_movie` with `tmdb_movie_details` if you would like to fetch and use the TMDb show's summary, poster, and background from the list +2. You can specify multiple shows in `tmdb_movie_details` but it will only use the first one to update the collection details +3. https://www.themoviedb.org/movie/630-the-wizard-of-oz also accepted + +* Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. + diff --git a/docs/files/builders/tmdb/network.md b/docs/files/builders/tmdb/network.md new file mode 100644 index 000000000..7ab0214c1 --- /dev/null +++ b/docs/files/builders/tmdb/network.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# TMDb Network + +Finds every item from the TMDb network's movie/show list. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example TMDb Network Builder(s) + +```yaml +collections: + CBS: + tmdb_network: 16 #(1)! +``` + +1. https://www.themoviedb.org/network/16 also accepted diff --git a/docs/files/builders/tmdb/now-playing.md b/docs/files/builders/tmdb/now-playing.md new file mode 100644 index 000000000..ce26409ef --- /dev/null +++ b/docs/files/builders/tmdb/now-playing.md @@ -0,0 +1,24 @@ +--- +hide: + - toc +--- +# TMDb Now Playing + +Finds the movies in TMDb's [Now Playing](https://www.themoviedb.org/movie/now-playing) list. + +Use `tmdb_region` with this Builder to set the region. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Now Playing Builder(s) + +```yaml +collections: + TMDb Now Playing: + tmdb_now_playing: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/on-the-air.md b/docs/files/builders/tmdb/on-the-air.md new file mode 100644 index 000000000..ed09767e7 --- /dev/null +++ b/docs/files/builders/tmdb/on-the-air.md @@ -0,0 +1,22 @@ +--- +hide: + - toc +--- +# TMDb On the Air + +Finds the shows in TMDb's [On TV Shows](https://www.themoviedb.org/tv/on-the-air) list. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb On the Air Builder(s) + +```yaml +collections: + TMDb On the Air: + tmdb_on_the_air: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/overview.md b/docs/files/builders/tmdb/overview.md new file mode 100644 index 000000000..851609bf5 --- /dev/null +++ b/docs/files/builders/tmdb/overview.md @@ -0,0 +1,50 @@ +--- +hide: + - toc +--- +# TMDb Builders + +You can find items using the features of [TheMovieDb.org](https://www.themoviedb.org/) (TMDb). + +## TMDb Standard Builders + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:---------------------------------------------------------------------|:---------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| `tmdb_discover`([movie](discover/movie.md)/[show](discover/show.md)) | Uses [TMDb's Discover Search](https://developer.themoviedb.org/docs/search-and-query-for-details) to find every movie/show based on the [movie search parameters](https://developers.themoviedb.org/3/discover/movie-discover) or [show search parameters](https://developers.themoviedb.org/3/discover/tv-discover) provided | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_collection`](collection.md) | Finds every item in the TMDb collection | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_list`](list.md) | Finds every item in the TMDb List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_movie`](movie.md) | Finds the movie specified | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_show`](show.md) | Finds the show specified | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_company`](company.md) | Finds every item from the TMDb company's movie/show list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_network`](network.md) | Finds every item from the TMDb network's show list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_keyword`](keyword.md) | Finds every item from the TMDb keyword's movie/show list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + +## TMDb Chart Builders + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------:|:------------------------------------------:|:-------------------------------------------:| +| [`tmdb_popular`](popular.md) | Finds the movies/shows in TMDb's [Popular Movies](https://www.themoviedb.org/movie)/[Popular Shows](https://www.themoviedb.org/tv) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_now_playing`](now-playing.md) | Finds the movies in TMDb's [Now Playing](https://www.themoviedb.org/movie/now-playing) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_top_rated`](top-rated.md) | Finds the movies/shows in TMDb's [Top Rated Movies](https://www.themoviedb.org/movie/top-rated)/[Top Rated Shows](https://www.themoviedb.org/tv/top-rated) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_upcoming`](upcoming.md) | Finds the movies in TMDb's [Upcoming Movies](https://www.themoviedb.org/movie/upcoming) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_airing_today`](airing-today.md) | Finds the shows in TMDb's [Airing Today Shows](https://www.themoviedb.org/tv/airing-today) list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_on_the_air`](on-the-air.md) | Finds the shows in TMDb's [On TV Shows](https://www.themoviedb.org/tv/on-the-air) list | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_trending_daily`](trending-daily.md) | Finds the movies/shows in TMDb's Trending Daily list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tmdb_trending_weekly`](trending-weekly.md) | Finds the movies/shows in TMDb's Trending Weekly list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | + + +## TMDb People Builders + + ???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:--------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------:|:------------------------------------------:|:-------------------------------------------:| +| [`tmdb_actor`](actor.md) | Finds every item in the TMDb Person's Actor Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_crew`](crew.md) | Finds every item in the TMDb Person's Crew Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_director`](director.md) | Finds every item in the TMDb Person's Director Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_producer`](producer.md) | Finds every item in the TMDb Person's Producer Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tmdb_writer`](writer.md) | Finds every item in the TMDb Person's Writer Credits | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/files/builders/tmdb/popular.md b/docs/files/builders/tmdb/popular.md new file mode 100644 index 000000000..52adf33c0 --- /dev/null +++ b/docs/files/builders/tmdb/popular.md @@ -0,0 +1,24 @@ +--- +hide: + - toc +--- +# TMDb Popular + +Finds the movies/shows in TMDb's [Popular Movies](https://www.themoviedb.org/movie)/[Popular Shows](https://www.themoviedb.org/tv) list. + +Use `tmdb_region` with this Builder to set the region. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Popular Builder(s) + +```yaml +collections: + TMDb Popular: + tmdb_popular: 30 + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/tmdb/producer.md b/docs/files/builders/tmdb/producer.md new file mode 100644 index 000000000..8c8cd2824 --- /dev/null +++ b/docs/files/builders/tmdb/producer.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# TMDb Producer + +???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +Finds every item in the TMDb Person's Producer Credits. + +### Example TMDb Producer Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Adam Sandler: + tmdb_producer: 19292 #(3)! + The Wachowskis: + tmdb_producer_details: #(1)! + - 9339 #(2)! + - 9340 +``` + +1. You can replace `tmdb_producer` with `tmdb_producer_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list +2. You can specify multiple producers in `tmdb_producer_details` but it will only use the first one to update the collection details +3. https://www.themoviedb.org/person/19292-adam-sandler also accepted diff --git a/docs/files/builders/tmdb/show.md b/docs/files/builders/tmdb/show.md new file mode 100644 index 000000000..c30934da2 --- /dev/null +++ b/docs/files/builders/tmdb/show.md @@ -0,0 +1,29 @@ +--- +hide: + - toc +--- +# TMDb Show + +Finds the show specified. + +This Builder is expected to have the full URL to the item or the TMDb ID of the item. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string. + +### Example TMDb Show Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Star Wars (Animated Shows): + tmdb_show: + - 4194 #(1)! + - 60554 + Pokémon Evolutions & Chronicles: + tmdb_show_details: #(2)! + - 132636 #(3)! + - 13230 +``` + +1. https://www.themoviedb.org/tv/4194-star-wars-the-clone-wars also accepted +2. You can replace `tmdb_show` with `tmdb_show_details` if you would like to fetch and use the TMDb show's summary, poster, and background from the list +3. You can specify multiple shows in `tmdb_show_details` but it will only use the first one to update the collection details + +* Posters and background in the library's asset directory will be used over the collection details unless `tmdb_poster`/`tmdb_background` is also specified. diff --git a/docs/files/builders/tmdb/top-rated.md b/docs/files/builders/tmdb/top-rated.md new file mode 100644 index 000000000..3d86f7184 --- /dev/null +++ b/docs/files/builders/tmdb/top-rated.md @@ -0,0 +1,24 @@ +--- +hide: + - toc +--- +# TMDb Top Rated + +Finds the movies/shows in TMDb's [Top Rated Movies](https://www.themoviedb.org/movie/top-rated)/[Top Rated Shows](https://www.themoviedb.org/tv/top-rated) list. + +Use `tmdb_region` with this Builder to set the region. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Top Rated Builder(s) + +```yaml +collections: + TMDb Top Rated: + tmdb_top_rated: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/trending-daily.md b/docs/files/builders/tmdb/trending-daily.md new file mode 100644 index 000000000..7ddb24b9d --- /dev/null +++ b/docs/files/builders/tmdb/trending-daily.md @@ -0,0 +1,22 @@ +--- +hide: + - toc +--- +# TMDb Trending Daily + +Finds the movies/shows in TMDb's Trending Daily list. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Trending Daily Builder(s) + +```yaml +collections: + TMDb Daily Trending: + tmdb_trending_daily: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/trending-weekly.md b/docs/files/builders/tmdb/trending-weekly.md new file mode 100644 index 000000000..ec743c26d --- /dev/null +++ b/docs/files/builders/tmdb/trending-weekly.md @@ -0,0 +1,22 @@ +--- +hide: + - toc +--- +# TMDb Trending Weekly + +Finds the movies/shows in TMDb's Trending Weekly list. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Trending Weekly Builder(s) + +```yaml +collections: + TMDb Weekly Trending: + tmdb_trending_weekly: 30 + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/tmdb/upcoming.md b/docs/files/builders/tmdb/upcoming.md new file mode 100644 index 000000000..43e33e0b1 --- /dev/null +++ b/docs/files/builders/tmdb/upcoming.md @@ -0,0 +1,24 @@ +--- +hide: + - toc +--- +# TMDb Upcoming + +Finds the movies in TMDb's [Upcoming Movies](https://www.themoviedb.org/movie/upcoming) list. + +Use `tmdb_region` with this Builder to set the region. + +This Builder is expected to have an integer (number) value of how many items to query + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +### Example TMDb Upcoming Builder(s) + +```yaml +collections: + TMDb Upcoming: + tmdb_upcoming: 30 + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tmdb/writer.md b/docs/files/builders/tmdb/writer.md new file mode 100644 index 000000000..827de40fc --- /dev/null +++ b/docs/files/builders/tmdb/writer.md @@ -0,0 +1,30 @@ +--- +hide: + - toc +--- +# TMDb Writer + +???+ tip "People Collections" + + As Plex does not allow People to be part of Collections, Kometa will instead add any media that the person is associated with based om the Builder criteria. + + You can not have a Collection of "Top 10 Actors" for example, as Plex does not allow this. + +Finds every item in the TMDb Person's Writer Credits. + +### Example TMDb Writer Builder(s) + +```yaml title="Press the + icon to learn more" +collections: + Woody Allen: + tmdb_writer: + - 1243 #(3)! + The Daniels: + tmdb_writer_details: #(1)! + - 1383612 #(2)! + - https://www.themoviedb.org/person/1317730 +``` + +1. You can replace `tmdb_writer` with `tmdb_writer_details` if you would like to fetch and use the TMDb collection's summary, poster, and background from the list +2. You can specify multiple people in `tmdb_writer_details` but it will only use the first one to update the collection details +3. https://www.themoviedb.org/person/1243-woody-allen also accepted \ No newline at end of file diff --git a/docs/files/builders/trakt/box-office.md b/docs/files/builders/trakt/box-office.md new file mode 100644 index 000000000..84cb13ce0 --- /dev/null +++ b/docs/files/builders/trakt/box-office.md @@ -0,0 +1,26 @@ +--- +hide: + - toc +--- +# Trakt Box Office + +Finds the 10 movies in Trakt's Top Box Office [Movies](https://trakt.tv/movies/boxoffice) list. + +The expected input is true. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ warning "Trakt Configuration" + + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +### Example Trakt Box Office Builder(s) + +```yaml +collections: + Trakt Collected: + trakt_boxoffice: true + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/trakt.md b/docs/files/builders/trakt/chart.md similarity index 64% rename from docs/files/builders/trakt.md rename to docs/files/builders/trakt/chart.md index 92ed31c00..15b22c7cd 100644 --- a/docs/files/builders/trakt.md +++ b/docs/files/builders/trakt/chart.md @@ -2,253 +2,73 @@ hide: - toc --- -# Trakt Builders +# Trakt Chart -You can find items using the features of [Trakt.tv](https://trakt.tv/) (Trakt). +Finds the movies/shows in the Trakt Chart. The options are detailed below. -???+ warning "Trakt Configuration" - - [Configuring Trakt](../../config/trakt.md) in the config is required for any of these builders. - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:--------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`trakt_list`](#trakt-list) | Finds every movie/show in the Trakt List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`trakt_chart`](#trakt-chart) | Finds the movies/shows in the Trakt Chart | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`trakt_userlist`](#trakt-userlist) | Finds every movie/show in the Trakt UserList | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`trakt_recommendations`](#trakt-recommendations) | Finds the movies/shows in Trakt's Personal Recommendations for your User [Movies](https://trakt.docs.apiary.io/#reference/recommendations/movies/get-movie-recommendations)/[Shows](https://trakt.docs.apiary.io/#reference/recommendations/shows/get-show-recommendations) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`trakt_boxoffice`](#trakt-box-office) | Finds the 10 movies in Trakt's Top Box Office [Movies](https://trakt.tv/movies/boxoffice) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - -=== "Trakt List" - - Finds every item in the Trakt List. - - The expected input is a Trakt List URL. Multiple values are supported only as a list. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - **Trakt Lists cannot be sorted through the API, but the list is always returned to the default list order if you own - the list.** - - ???+ tip "Details Builder" - - You can replace `trakt_list` with `trakt_list_details` if you would like to fetch and use the description from the list - - ???+ tip "Private Trakt Lists" - - If you have [authorized Trakt](../../config/trakt.md) then you can use private Trakt Lists, this is not possible if - you have not authorized Trakt. - - When you link to a private list, set the list to `private` and then use the standard browser link: - - ``` - https://trakt.tv/users/YOURTRAKTUSERNAME/lists/YOURLISTNAME - ``` - - DO NOT set the list to `Share` and attempt to use the "Share link"; Kometa cannot use that address for the list. - - - ???+ warning - - Trakt lists and users come and go, and Kometa has no control over this. The list URLs found in this documentation - are used here as examples and are available and working at time of writing, but they may disappear at any time. Do not take their use here as a guarantee that they exist or are working when you read this. - - ### Example Trakt List Builder(s) - - ```yaml - collections: - Christmas: - trakt_list: - - https://trakt.tv/users/movistapp/lists/christmas-movies - - https://trakt.tv/users/2borno2b/lists/christmas-movies-extravanganza - sync_mode: sync - ``` - ```yaml - collections: - Reddit Top 250: - trakt_list: https://trakt.tv/users/jaygreene/lists/reddit-top-250-2019-edition - collection_order: custom - sync_mode: sync - ``` - - * You can update the collection details with the Trakt List's description by using `trakt_list_details`. - * You can specify multiple collections in `trakt_list_details` but it will only use the first one to update the collection summary. - - ```yaml - collections: - Reddit Top 250: - trakt_list_details: https://trakt.tv/users/jaygreene/lists/reddit-top-250-2019-edition - collection_order: custom - sync_mode: sync - ``` - -=== "Trakt Chart" - - Finds the movies/shows in the Trakt Chart. The options are detailed below. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - | Attribute | Description & Values | - |:-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `chart` | **Description:** Which Trakt chart to query
**Values:**
`trending`Trakt's Trending [Movies](https://trakt.tv/movies/trending)/[Shows](https://trakt.tv/shows/trending) list
`popular`Trakt's Popular [Movies](https://trakt.tv/movies/popular)/[Shows](https://trakt.tv/shows/popular) list
`recommended`Trakt's Recommended [Movies](https://trakt.tv/movies/recommended)/[Shows](https://trakt.tv/shows/recommended) list
`watched`Trakt's Watched [Movies](https://trakt.tv/movies/watched)/[Shows](https://trakt.tv/shows/watched) list
`collected`Trakt's Collected [Movies](https://trakt.tv/movies/collected)/[Shows](https://trakt.tv/shows/collected) list
| - | `time_period` | **Description:** Time Period for the chart. Does not work with `trending` or `popular` chart types.
**Default:** `weekly`
**Values:** `daily`, `weekly`, `monthly`, `yearly`, or `all` | - | `limit` | **Description:** Don't return more than this number
**Default:** `10`
**Values:** Number of Items to query. | - | `query` | **Description:** Search titles and descriptions for this
**Values:** Any String. | - | `years` | **Description:** Search for the specified years only
**Values:** 4 digit year or range of 4 digit years. i.e. `1950` or `1950-1959` | - | `genres` | **Description:** Search for the specified genres only
**Values:** Comma separated string or list of genres
**Movie Genres:** `action`, `adventure`, `animation`, `anime`, `comedy`, `crime`, `documentary`, `drama`, `family`, `fantasy`, `history`, `holiday`, `horror`, `music`, `musical`, `mystery`, `none`, `romance`, `science-fiction`, `short`, `sporting-event`, `superhero`, `suspense`, `thriller`, `war`, `western`
**Show Genres:** `action`, `adventure`, `animation`, `anime`, `biography`, `children`, `comedy`, `crime`, `documentary`, `drama`, `family`, `fantasy`, `game-show`, `history`, `holiday`, `home-and-garden`, `horror`, `mini-series`, `music`, `musical`, `mystery`, `news`, `none`, `reality`, `romance`, `science-fiction`, `short`, `soap`, `special-interest`, `sporting-event`, `superhero`, `suspense`, `talk-show`, `thriller`, `war`, `western` | - | `languages` | **Description:** Search for the specified languages only
**Values:** Comma separated string or list of languages
**Movie Languages:** `ab`, `af`, `ak`, `sq`, `am`, `ar`, `an`, `hy`, `as`, `av`, `ay`, `az`, `bm`, `ba`, `eu`, `be`, `bn`, `bi`, `nb`, `bs`, `bg`, `my`, `ca`, `km`, `ch`, `ce`, `ny`, `zh`, `kw`, `co`, `cr`, `hr`, `cs`, `da`, `dv`, `nl`, `dz`, `en`, `eo`, `et`, `fo`, `fj`, `fi`, `fr`, `ff`, `gd`, `gl`, `lg`, `ka`, `de`, `el`, `gn`, `gu`, `ht`, `ha`, `he`, `hi`, `hu`, `is`, `ig`, `id`, `ie`, `iu`, `ik`, `ga`, `it`, `ja`, `jv`, `kl`, `kn`, `ks`, `kk`, `rw`, `ky`, `kg`, `ko`, `ku`, `lo`, `la`, `lv`, `li`, `ln`, `lt`, `lb`, `mk`, `mg`, `ms`, `ml`, `mt`, `mi`, `mr`, `mh`, `mn`, `nv`, `ne`, `se`, `no`, `nn`, `oc`, `oj`, `or`, `om`, `os`, `pi`, `pa`, `fa`, `pl`, `pt`, `ps`, `qu`, `ro`, `rm`, `rn`, `ru`, `sm`, `sg`, `sa`, `sc`, `sr`, `sn`, `ii`, `sd`, `si`, `sk`, `sl`, `so`, `st`, `es`, `su`, `sw`, `ss`, `sv`, `tl`, `ty`, `tg`, `ta`, `tt`, `te`, `th`, `bo`, `ti`, `to`, `ts`, `tn`, `tr`, `tk`, `tw`, `ug`, `uk`, `ur`, `uz`, `vi`, `cy`, `fy`, `wo`, `xh`, `yi`, `yo`, `za`, `zu`
**Show Languages:** `ab`, `af`, `sq`, `am`, `ar`, `hy`, `eu`, `be`, `bn`, `nb`, `bs`, `bg`, `ca`, `km`, `zh`, `hr`, `cs`, `da`, `dv`, `nl`, `en`, `et`, `fi`, `fr`, `gl`, `ka`, `de`, `el`, `gu`, `he`, `hi`, `hu`, `is`, `id`, `ga`, `it`, `ja`, `kn`, `ko`, `lo`, `la`, `lv`, `lt`, `lb`, `mk`, `ms`, `ml`, `mt`, `mi`, `mr`, `ne`, `se`, `no`, `nn`, `pa`, `fa`, `pl`, `pt`, `ro`, `ru`, `sr`, `si`, `sk`, `sl`, `es`, `sv`, `tl`, `ta`, `te`, `th`, `tr`, `tw`, `uk`, `ur`, `uz`, `vi`, `cy` | - | `countries` | **Description:** Search for the specified countries only
**Values:** Comma separated string or list of countries
**Movie Countries:** `af`, `al`, `dz`, `as`, `ad`, `ao`, `ai`, `aq`, `ag`, `ar`, `am`, `aw`, `au`, `at`, `az`, `bs`, `bh`, `bd`, `bb`, `by`, `be`, `bz`, `bj`, `bm`, `bt`, `bo`, `ba`, `bw`, `bv`, `br`, `io`, `bn`, `bg`, `bf`, `bi`, `cv`, `kh`, `cm`, `ca`, `ky`, `cf`, `td`, `cl`, `cn`, `cx`, `co`, `km`, `cg`, `cd`, `ck`, `cr`, `hr`, `cu`, `cy`, `cz`, `ci`, `dk`, `dj`, `dm`, `do`, `ec`, `eg`, `sv`, `gq`, `er`, `ee`, `sz`, `et`, `fk`, `fo`, `fj`, `fi`, `fr`, `gf`, `pf`, `tf`, `ga`, `gm`, `ge`, `de`, `gh`, `gi`, `gr`, `gl`, `gd`, `gp`, `gu`, `gt`, `gn`, `gw`, `gy`, `ht`, `va`, `hn`, `hk`, `hu`, `is`, `in`, `id`, `ir`, `iq`, `ie`, `il`, `it`, `jm`, `jp`, `jo`, `kz`, `ke`, `ki`, `kp`, `kr`, `kw`, `kg`, `la`, `lv`, `lb`, `ls`, `lr`, `ly`, `li`, `lt`, `lu`, `mo`, `mg`, `mw`, `my`, `mv`, `ml`, `mt`, `mh`, `mq`, `mr`, `mu`, `yt`, `mx`, `md`, `mc`, `mn`, `me`, `ms`, `ma`, `mz`, `mm`, `na`, `nr`, `np`, `nl`, `nc`, `nz`, `ni`, `ne`, `ng`, `nf`, `mk`, `mp`, `no`, `om`, `pk`, `pw`, `ps`, `pa`, `pg`, `py`, `pe`, `ph`, `pn`, `pl`, `pt`, `pr`, `qa`, `ro`, `ru`, `rw`, `re`, `sh`, `kn`, `lc`, `vc`, `ws`, `sm`, `st`, `sa`, `sn`, `rs`, `sc`, `sl`, `sg`, `sk`, `si`, `sb`, `so`, `za`, `ss`, `es`, `lk`, `sd`, `sr`, `se`, `ch`, `sy`, `tw`, `tj`, `tz`, `th`, `tl`, `tg`, `tk`, `to`, `tt`, `tn`, `tr`, `tm`, `tc`, `tv`, `ug`, `ua`, `ae`, `gb`, `us`, `um`, `uy`, `uz`, `vu`, `ve`, `vn`, `vg`, `vi`, `wf`, `eh`, `ye`, `zm`, `zw`
**Show Countries:** `af`, `ad`, `ar`, `am`, `au`, `at`, `bd`, `by`, `be`, `bz`, `ba`, `bw`, `br`, `io`, `bg`, `kh`, `ca`, `td`, `cl`, `cn`, `co`, `hr`, `cu`, `cy`, `cz`, `dk`, `do`, `ec`, `eg`, `ee`, `sz`, `fi`, `fr`, `ge`, `de`, `gr`, `hn`, `hk`, `hu`, `is`, `in`, `id`, `ir`, `iq`, `ie`, `il`, `it`, `jp`, `jo`, `kz`, `kp`, `kr`, `kw`, `la`, `lv`, `lb`, `lt`, `lu`, `my`, `mv`, `mt`, `mx`, `md`, `mc`, `me`, `ma`, `np`, `nl`, `nz`, `ng`, `mk`, `mp`, `no`, `pk`, `pa`, `py`, `pe`, `ph`, `pl`, `pt`, `pr`, `qa`, `ro`, `ru`, `sa`, `sn`, `rs`, `sg`, `sk`, `si`, `za`, `es`, `lk`, `se`, `ch`, `sy`, `tw`, `th`, `tg`, `tn`, `tr`, `ua`, `ae`, `gb`, `us`, `uy`, `ve`, `vn` | - | `certifications` | **Description:** Search for the specified certifications only
**Values:** Comma separated string or list of certifications
**Movie Certifications:** `g`, `pg`, `pg-13`, `r`, `nr`
**Show Certifications:** `tv-y`, `tv-y7`, `tv-g`, `tv-pg`, `tv-14`, `tv-ma`, `nr` | - | `runtimes` | **Description:** Search for the specified runtime range
**Values:** range of int i.e. `0-60` | - | `ratings` | **Description:** Search for the specified Trakt rating range
**Values:** range of int from `0-100` i.e. `80-100` | - | `votes` | **Description:** Search for the specified Trakt vote count range
**Values:** range of int from `0-100000` i.e. `80-100` | - | `tmdb_ratings` | **Description:** Search for the specified TMDb rating range
**Values:** range of float from `0.0-10.0` i.e. `8.5-10.0` | - | `tmdb_votes` | **Description:** Search for the specified TMDb vote count range
**Values:** range of int from `0-100000` i.e. `8.5-10.0` | - | `imdb_ratings` | **Description:** Search for the specified IMDb rating range
**Values:** range of float from `0.0-10.0` i.e. `80-100` | - | `imdb_votes` | **Description:** Search for the specified IMDb vote count range
**Values:** range of int from `0-3000000` i.e. `80-100` | - | `rt_meters` | **Description:** Search for the specified Rotten Tomatoes tomatometer range
**Values:** range of int from `0-100` i.e. `80-100` | - | `rt_user_meters` | **Description:** Search for the specified Rotten Tomatoes audience score range
**Values:** range of int from `0-100` i.e. `80-100` | - | `metascores` | **Description:** Search for the specified Metacritic score range
**Values:** range of int from `0-100` i.e. `80-100` | - | `studio_ids` | **Description:** Search for the specified Studio IDs only
**Values:** Comma separated string or list of Studio IDs | - | `network_ids` | **Description:** Search for the specified Network IDs only **Only works with shows**
**Values:** Comma separated string or list of Network IDs | - | `status` | **Description:** Search for the specified status only **Only works with shows**
**Values:** Comma separated string or list of statuses
**Status:** `returning`, `production`, `planned`, `canceled`, `ended` | - - These are the links to the trakt charts that is looked at by time period. - - | Time Period | Collected | Recommended | Watched | - |:------------|:-----------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------:| - | Daily | [Movies](https://trakt.tv/movies/collected/daily)/[Shows](https://trakt.tv/shows/collected/daily) | [Movies](https://trakt.tv/movies/recommended/daily)/[Shows](https://trakt.tv/shows/recommended/daily) | [Movies](https://trakt.tv/movies/watched/daily)/[Shows](https://trakt.tv/shows/watched/daily) | - | Weekly | [Movies](https://trakt.tv/movies/collected/weekly)/[Shows](https://trakt.tv/shows/collected/weekly) | [Movies](https://trakt.tv/movies/recommended/weekly)/[Shows](https://trakt.tv/shows/recommended/weekly) | [Movies](https://trakt.tv/movies/watched/weekly)/[Shows](https://trakt.tv/shows/watched/weekly) | - | Monthly | [Movies](https://trakt.tv/movies/collected/monthly)/[Shows](https://trakt.tv/shows/collected/monthly) | [Movies](https://trakt.tv/movies/recommended/monthly)/[Shows](https://trakt.tv/shows/recommended/monthly) | [Movies](https://trakt.tv/movies/watched/monthly)/[Shows](https://trakt.tv/shows/watched/monthly) | - | Yearly | [Movies](https://trakt.tv/movies/collected/yearly)/[Shows](https://trakt.tv/shows/collected/yearly) | [Movies](https://trakt.tv/movies/recommended/yearly)/[Shows](https://trakt.tv/shows/recommended/yearly) | [Movies](https://trakt.tv/movies/watched/yearly)/[Shows](https://trakt.tv/shows/watched/yearly) | - | All-Time | [Movies](https://trakt.tv/movies/collected/all)/[Shows](https://trakt.tv/shows/collected/all) | [Movies](https://trakt.tv/movies/recommended/all)/[Shows](https://trakt.tv/shows/recommended/all) | [Movies](https://trakt.tv/movies/watched/all)/[Shows](https://trakt.tv/shows/watched/all) | - - ### Example Trakt Chart Builder(s) +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. - ```yaml - collections: - Trakt Trending: - trakt_chart: - chart: trending - limit: 30 - collection_order: custom - sync_mode: sync - ``` - - You can use multiple charts in one Builder using a list. - - ```yaml - collections: - Trakt Trending & Popular: - trakt_chart: - - chart: trending - limit: 30 - - chart: popular - limit: 30 - sync_mode: sync - ``` - -=== "Trakt UserList" - - Finds every movie/show in the Trakt UserList. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - | Attribute | Description & Values | - |:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `userlist` | **Description:** Which Trakt userlist to query
**Values:**
`watchlist`Trakt User's Watchlist
`favorites`Trakt User's Personal Favorite list
`watched`Trakt User's Personal Watched list
`collection`Trakt User's Personal Collection list
| - | `user` | **Description:** The User who's user lists you want to query.
**Default:** `me`
**Values:** Username of User or `me` for the authenticated user. | - | `sort_by` | **Description:** How to sort the results
**Default:** `rank`
**Values:** `rank`, `added`, `released`, `title` | - - ### Example Trakt UserList Builder(s) - - ```yaml - collections: - Trakt Watchlist: - trakt_userlist: - userlist: watchlist - user: me - sort_by: released - collection_order: custom - sync_mode: sync - ``` - - You can use multiple charts in one Builder using a list. - - ```yaml - collections: - Trakt Watchlist: - trakt_userlist: - - userlist: watched - user: me - - userlist: collection - user: me - collection_order: custom - sync_mode: sync - ``` - -=== "Trakt Recommendations" - - Finds the movies/shows in Trakt's Recommendations for [Movies](https://trakt.docs.apiary.io/#reference/recommendations/movies/get-movie-recommendations)/[Shows](https://trakt.docs.apiary.io/#reference/recommendations/shows/get-show-recommendations) - - The expected input is a single integer value of how many movies/shows to query. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example Trakt Recommendations Builder(s) - - ```yaml - collections: - Trakt Recommendations: - trakt_recommendations: 30 - collection_order: custom - sync_mode: sync - ``` - -=== "Trakt Box Office" - - Finds the 10 movies in Trakt's Top Box Office [Movies](https://trakt.tv/movies/boxoffice) list. - - The expected input is true. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ### Example Trakt Box Office Builder(s) - - ```yaml - collections: - Trakt Collected: - trakt_boxoffice: true - collection_order: custom - sync_mode: sync - ``` - -=== "Syncing Plex Collections to Trakt Lists" - - A combination of Kometa settings/attributes can be utilized to create a collection via Kometa and then sync them to a - blank or existing Trakt List. - - NOTE: You must either create an empty Trakt list or specify an existing Trakt list which you have write access to. - - ### Example Plex Collection to Trakt List Sync +???+ warning "Trakt Configuration" - ```yaml - collections: - My Favourite Movies: - sync_to_trakt_list: myfilms - # trakt_list: https://trakt.tv/users/k0meta/lists/myfilms OPTIONAL TWO-WAY SYNC - plex_search: - any: - rating.gte: 8 - sort_by: user_rating.desc - ``` - - In this example, Kometa will generate a Plex collection with any films that I have rated 8.0 or above (using the - `plex_search`). The `sync_to_trakt_list` will take those films and then sync them to my `myfilms` Trakt list. - - I can optionally also enable the `trakt_list` to create a two-way sync between Plex and Trakt. This allows me to add - films to the Trakt list that I want in my Plex collection, and then sync them to Plex. + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +| Attribute | Description & Values | +|:-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `chart` | **Description:** Which Trakt chart to query
**Values:**
`trending`Trakt's Trending [Movies](https://trakt.tv/movies/trending)/[Shows](https://trakt.tv/shows/trending) list
`popular`Trakt's Popular [Movies](https://trakt.tv/movies/popular)/[Shows](https://trakt.tv/shows/popular) list
`recommended`Trakt's Recommended [Movies](https://trakt.tv/movies/recommended)/[Shows](https://trakt.tv/shows/recommended) list
`watched`Trakt's Watched [Movies](https://trakt.tv/movies/watched)/[Shows](https://trakt.tv/shows/watched) list
`collected`Trakt's Collected [Movies](https://trakt.tv/movies/collected)/[Shows](https://trakt.tv/shows/collected) list
| +| `time_period` | **Description:** Time Period for the chart. Does not work with `trending` or `popular` chart types.
**Default:** `weekly`
**Values:** `daily`, `weekly`, `monthly`, `yearly`, or `all` | +| `limit` | **Description:** Don't return more than this number
**Default:** `10`
**Values:** Number of Items to query. | +| `query` | **Description:** Search titles and descriptions for this
**Values:** Any String. | +| `years` | **Description:** Search for the specified years only
**Values:** 4 digit year or range of 4 digit years. i.e. `1950` or `1950-1959` | +| `genres` | **Description:** Search for the specified genres only
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of genres
**Movie Genres:** `action`, `adventure`, `animation`, `anime`, `comedy`, `crime`, `documentary`, `drama`, `family`, `fantasy`, `history`, `holiday`, `horror`, `music`, `musical`, `mystery`, `none`, `romance`, `science-fiction`, `short`, `sporting-event`, `superhero`, `suspense`, `thriller`, `war`, `western`
**Show Genres:** `action`, `adventure`, `animation`, `anime`, `biography`, `children`, `comedy`, `crime`, `documentary`, `drama`, `family`, `fantasy`, `game-show`, `history`, `holiday`, `home-and-garden`, `horror`, `mini-series`, `music`, `musical`, `mystery`, `news`, `none`, `reality`, `romance`, `science-fiction`, `short`, `soap`, `special-interest`, `sporting-event`, `superhero`, `suspense`, `talk-show`, `thriller`, `war`, `western` | +| `languages` | **Description:** Search for the specified languages only
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of languages
**Movie Languages:** `ab`, `af`, `ak`, `sq`, `am`, `ar`, `an`, `hy`, `as`, `av`, `ay`, `az`, `bm`, `ba`, `eu`, `be`, `bn`, `bi`, `nb`, `bs`, `bg`, `my`, `ca`, `km`, `ch`, `ce`, `ny`, `zh`, `kw`, `co`, `cr`, `hr`, `cs`, `da`, `dv`, `nl`, `dz`, `en`, `eo`, `et`, `fo`, `fj`, `fi`, `fr`, `ff`, `gd`, `gl`, `lg`, `ka`, `de`, `el`, `gn`, `gu`, `ht`, `ha`, `he`, `hi`, `hu`, `is`, `ig`, `id`, `ie`, `iu`, `ik`, `ga`, `it`, `ja`, `jv`, `kl`, `kn`, `ks`, `kk`, `rw`, `ky`, `kg`, `ko`, `ku`, `lo`, `la`, `lv`, `li`, `ln`, `lt`, `lb`, `mk`, `mg`, `ms`, `ml`, `mt`, `mi`, `mr`, `mh`, `mn`, `nv`, `ne`, `se`, `no`, `nn`, `oc`, `oj`, `or`, `om`, `os`, `pi`, `pa`, `fa`, `pl`, `pt`, `ps`, `qu`, `ro`, `rm`, `rn`, `ru`, `sm`, `sg`, `sa`, `sc`, `sr`, `sn`, `ii`, `sd`, `si`, `sk`, `sl`, `so`, `st`, `es`, `su`, `sw`, `ss`, `sv`, `tl`, `ty`, `tg`, `ta`, `tt`, `te`, `th`, `bo`, `ti`, `to`, `ts`, `tn`, `tr`, `tk`, `tw`, `ug`, `uk`, `ur`, `uz`, `vi`, `cy`, `fy`, `wo`, `xh`, `yi`, `yo`, `za`, `zu`
**Show Languages:** `ab`, `af`, `sq`, `am`, `ar`, `hy`, `eu`, `be`, `bn`, `nb`, `bs`, `bg`, `ca`, `km`, `zh`, `hr`, `cs`, `da`, `dv`, `nl`, `en`, `et`, `fi`, `fr`, `gl`, `ka`, `de`, `el`, `gu`, `he`, `hi`, `hu`, `is`, `id`, `ga`, `it`, `ja`, `kn`, `ko`, `lo`, `la`, `lv`, `lt`, `lb`, `mk`, `ms`, `ml`, `mt`, `mi`, `mr`, `ne`, `se`, `no`, `nn`, `pa`, `fa`, `pl`, `pt`, `ro`, `ru`, `sr`, `si`, `sk`, `sl`, `es`, `sv`, `tl`, `ta`, `te`, `th`, `tr`, `tw`, `uk`, `ur`, `uz`, `vi`, `cy` | +| `countries` | **Description:** Search for the specified countries only
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of countries
**Movie Countries:** `af`, `al`, `dz`, `as`, `ad`, `ao`, `ai`, `aq`, `ag`, `ar`, `am`, `aw`, `au`, `at`, `az`, `bs`, `bh`, `bd`, `bb`, `by`, `be`, `bz`, `bj`, `bm`, `bt`, `bo`, `ba`, `bw`, `bv`, `br`, `io`, `bn`, `bg`, `bf`, `bi`, `cv`, `kh`, `cm`, `ca`, `ky`, `cf`, `td`, `cl`, `cn`, `cx`, `co`, `km`, `cg`, `cd`, `ck`, `cr`, `hr`, `cu`, `cy`, `cz`, `ci`, `dk`, `dj`, `dm`, `do`, `ec`, `eg`, `sv`, `gq`, `er`, `ee`, `sz`, `et`, `fk`, `fo`, `fj`, `fi`, `fr`, `gf`, `pf`, `tf`, `ga`, `gm`, `ge`, `de`, `gh`, `gi`, `gr`, `gl`, `gd`, `gp`, `gu`, `gt`, `gn`, `gw`, `gy`, `ht`, `va`, `hn`, `hk`, `hu`, `is`, `in`, `id`, `ir`, `iq`, `ie`, `il`, `it`, `jm`, `jp`, `jo`, `kz`, `ke`, `ki`, `kp`, `kr`, `kw`, `kg`, `la`, `lv`, `lb`, `ls`, `lr`, `ly`, `li`, `lt`, `lu`, `mo`, `mg`, `mw`, `my`, `mv`, `ml`, `mt`, `mh`, `mq`, `mr`, `mu`, `yt`, `mx`, `md`, `mc`, `mn`, `me`, `ms`, `ma`, `mz`, `mm`, `na`, `nr`, `np`, `nl`, `nc`, `nz`, `ni`, `ne`, `ng`, `nf`, `mk`, `mp`, `no`, `om`, `pk`, `pw`, `ps`, `pa`, `pg`, `py`, `pe`, `ph`, `pn`, `pl`, `pt`, `pr`, `qa`, `ro`, `ru`, `rw`, `re`, `sh`, `kn`, `lc`, `vc`, `ws`, `sm`, `st`, `sa`, `sn`, `rs`, `sc`, `sl`, `sg`, `sk`, `si`, `sb`, `so`, `za`, `ss`, `es`, `lk`, `sd`, `sr`, `se`, `ch`, `sy`, `tw`, `tj`, `tz`, `th`, `tl`, `tg`, `tk`, `to`, `tt`, `tn`, `tr`, `tm`, `tc`, `tv`, `ug`, `ua`, `ae`, `gb`, `us`, `um`, `uy`, `uz`, `vu`, `ve`, `vn`, `vg`, `vi`, `wf`, `eh`, `ye`, `zm`, `zw`
**Show Countries:** `af`, `ad`, `ar`, `am`, `au`, `at`, `bd`, `by`, `be`, `bz`, `ba`, `bw`, `br`, `io`, `bg`, `kh`, `ca`, `td`, `cl`, `cn`, `co`, `hr`, `cu`, `cy`, `cz`, `dk`, `do`, `ec`, `eg`, `ee`, `sz`, `fi`, `fr`, `ge`, `de`, `gr`, `hn`, `hk`, `hu`, `is`, `in`, `id`, `ir`, `iq`, `ie`, `il`, `it`, `jp`, `jo`, `kz`, `kp`, `kr`, `kw`, `la`, `lv`, `lb`, `lt`, `lu`, `my`, `mv`, `mt`, `mx`, `md`, `mc`, `me`, `ma`, `np`, `nl`, `nz`, `ng`, `mk`, `mp`, `no`, `pk`, `pa`, `py`, `pe`, `ph`, `pl`, `pt`, `pr`, `qa`, `ro`, `ru`, `sa`, `sn`, `rs`, `sg`, `sk`, `si`, `za`, `es`, `lk`, `se`, `ch`, `sy`, `tw`, `th`, `tg`, `tn`, `tr`, `ua`, `ae`, `gb`, `us`, `uy`, `ve`, `vn` | +| `certifications` | **Description:** Search for the specified certifications only
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of certifications
**Movie Certifications:** `g`, `pg`, `pg-13`, `r`, `nr`
**Show Certifications:** `tv-y`, `tv-y7`, `tv-g`, `tv-pg`, `tv-14`, `tv-ma`, `nr` | +| `runtimes` | **Description:** Search for the specified runtime range
**Values:** range of int i.e. `0-60` | +| `ratings` | **Description:** Search for the specified Trakt rating range
**Values:** range of int from `0-100` i.e. `80-100` | +| `votes` | **Description:** Search for the specified Trakt vote count range
**Values:** range of int from `0-100000` i.e. `80-100` | +| `tmdb_ratings` | **Description:** Search for the specified TMDb rating range
**Values:** range of float from `0.0-10.0` i.e. `8.5-10.0` | +| `tmdb_votes` | **Description:** Search for the specified TMDb vote count range
**Values:** range of int from `0-100000` i.e. `8.5-10.0` | +| `imdb_ratings` | **Description:** Search for the specified IMDb rating range
**Values:** range of float from `0.0-10.0` i.e. `80-100` | +| `imdb_votes` | **Description:** Search for the specified IMDb vote count range
**Values:** range of int from `0-3000000` i.e. `80-100` | +| `rt_meters` | **Description:** Search for the specified Rotten Tomatoes tomatometer range
**Values:** range of int from `0-100` i.e. `80-100` | +| `rt_user_meters` | **Description:** Search for the specified Rotten Tomatoes audience score range
**Values:** range of int from `0-100` i.e. `80-100` | +| `metascores` | **Description:** Search for the specified Metacritic score range
**Values:** range of int from `0-100` i.e. `80-100` | +| `studio_ids` | **Description:** Search for the specified Studio IDs only
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Studio IDs | +| `network_ids` | **Description:** Search for the specified Network IDs only **Only works with shows**
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Network IDs | +| `status` | **Description:** Search for the specified status only **Only works with shows**
**Values:** Comma separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of statuses
**Status:** `returning`, `production`, `planned`, `canceled`, `ended` | + +These are the links to the trakt charts that is looked at by time period. + +| Time Period | Collected | Recommended | Watched | +|:------------|:-----------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------:| +| Daily | [Movies](https://trakt.tv/movies/collected/daily)/[Shows](https://trakt.tv/shows/collected/daily) | [Movies](https://trakt.tv/movies/recommended/daily)/[Shows](https://trakt.tv/shows/recommended/daily) | [Movies](https://trakt.tv/movies/watched/daily)/[Shows](https://trakt.tv/shows/watched/daily) | +| Weekly | [Movies](https://trakt.tv/movies/collected/weekly)/[Shows](https://trakt.tv/shows/collected/weekly) | [Movies](https://trakt.tv/movies/recommended/weekly)/[Shows](https://trakt.tv/shows/recommended/weekly) | [Movies](https://trakt.tv/movies/watched/weekly)/[Shows](https://trakt.tv/shows/watched/weekly) | +| Monthly | [Movies](https://trakt.tv/movies/collected/monthly)/[Shows](https://trakt.tv/shows/collected/monthly) | [Movies](https://trakt.tv/movies/recommended/monthly)/[Shows](https://trakt.tv/shows/recommended/monthly) | [Movies](https://trakt.tv/movies/watched/monthly)/[Shows](https://trakt.tv/shows/watched/monthly) | +| Yearly | [Movies](https://trakt.tv/movies/collected/yearly)/[Shows](https://trakt.tv/shows/collected/yearly) | [Movies](https://trakt.tv/movies/recommended/yearly)/[Shows](https://trakt.tv/shows/recommended/yearly) | [Movies](https://trakt.tv/movies/watched/yearly)/[Shows](https://trakt.tv/shows/watched/yearly) | +| All-Time | [Movies](https://trakt.tv/movies/collected/all)/[Shows](https://trakt.tv/shows/collected/all) | [Movies](https://trakt.tv/movies/recommended/all)/[Shows](https://trakt.tv/shows/recommended/all) | [Movies](https://trakt.tv/movies/watched/all)/[Shows](https://trakt.tv/shows/watched/all) | + +### Example Trakt Chart Builder(s) + +```yaml +collections: + Trakt Trending: + trakt_chart: + chart: trending + limit: 30 + collection_order: custom + sync_mode: sync +``` + +You can use multiple charts in one Builder using a list. + +```yaml +collections: + Trakt Trending & Popular: + trakt_chart: + - chart: trending + limit: 30 + - chart: popular + limit: 30 + sync_mode: sync +``` diff --git a/docs/files/builders/trakt/list.md b/docs/files/builders/trakt/list.md new file mode 100644 index 000000000..e93b97493 --- /dev/null +++ b/docs/files/builders/trakt/list.md @@ -0,0 +1,67 @@ +--- +hide: + - toc +--- +# Trakt List + +Finds every item in the Trakt List. + +The expected input is a Trakt List URL. Multiple values are supported only as a list. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +**Trakt Lists cannot be sorted through the API, but the list is always returned to the default list order if you own +the list.** + +???+ warning "Trakt Configuration" + + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +You can replace `trakt_list` with `trakt_list_details` if you would like to fetch and use the description from the list + +If you have [authorized Trakt](../../../config/trakt.md) then you can use private Trakt Lists, this is not possible if +you have not authorized Trakt. + +When you link to a private list, set the list to `private` and then use the standard browser link: + +``` +https://trakt.tv/users/YOURTRAKTUSERNAME/lists/YOURLISTNAME +``` + +**DO NOT** set the list to `Share` and attempt to use the "Share link"; Kometa cannot use that address for the list. + + +???+ warning + + Trakt lists and users come and go, and Kometa has no control over this. The list URLs found in this documentation + are used here as examples and are available and working at time of writing, but they may disappear at any time. Do not take their use here as a guarantee that they exist or are working when you read this. + +### Example Trakt List Builder(s) + +```yaml +collections: + Christmas: + trakt_list: + - https://trakt.tv/users/movistapp/lists/christmas-movies + - https://trakt.tv/users/2borno2b/lists/christmas-movies-extravanganza + sync_mode: sync +``` +```yaml +collections: + Reddit Top 250: + trakt_list: https://trakt.tv/users/jaygreene/lists/reddit-top-250-2019-edition + collection_order: custom + sync_mode: sync +``` + +* You can update the collection details with the Trakt List's description by using `trakt_list_details`. +* You can specify multiple collections in `trakt_list_details` but it will only use the first one to update the collection summary. + +```yaml +collections: + Reddit Top 250: + trakt_list_details: https://trakt.tv/users/jaygreene/lists/reddit-top-250-2019-edition + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/trakt/overview.md b/docs/files/builders/trakt/overview.md new file mode 100644 index 000000000..94ab60bdc --- /dev/null +++ b/docs/files/builders/trakt/overview.md @@ -0,0 +1,19 @@ +--- +hide: + - toc +--- +# Trakt Builders + +You can find items using the features of [Trakt.tv](https://trakt.tv/) (Trakt). + +???+ warning "Trakt Configuration" + + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`trakt_list`](list.md) | Finds every movie/show in the Trakt List | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`trakt_chart`](chart.md) | Finds the movies/shows in the Trakt Chart | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`trakt_userlist`](userlist.md) | Finds every movie/show in the Trakt UserList | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`trakt_recommendations`](recommendations.md) | Finds the movies/shows in Trakt's Personal Recommendations for your User [Movies](https://trakt.docs.apiary.io/#reference/recommendations/movies/get-movie-recommendations)/[Shows](https://trakt.docs.apiary.io/#reference/recommendations/shows/get-show-recommendations) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`trakt_boxoffice`](box-office.md) | Finds the 10 movies in Trakt's Top Box Office [Movies](https://trakt.tv/movies/boxoffice) list | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | diff --git a/docs/files/builders/trakt/recommendations.md b/docs/files/builders/trakt/recommendations.md new file mode 100644 index 000000000..3689e655b --- /dev/null +++ b/docs/files/builders/trakt/recommendations.md @@ -0,0 +1,26 @@ +--- +hide: + - toc +--- +# Trakt Recommendations + +Finds the movies/shows in Trakt's Recommendations for [Movies](https://trakt.docs.apiary.io/#reference/recommendations/movies/get-movie-recommendations)/[Shows](https://trakt.docs.apiary.io/#reference/recommendations/shows/get-show-recommendations) + +The expected input is a single integer value of how many movies/shows to query. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ warning "Trakt Configuration" + + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +### Example Trakt Recommendations Builder(s) + +```yaml +collections: + Trakt Recommendations: + trakt_recommendations: 30 + collection_order: custom + sync_mode: sync +``` \ No newline at end of file diff --git a/docs/files/builders/trakt/userlist.md b/docs/files/builders/trakt/userlist.md new file mode 100644 index 000000000..c244e9dd5 --- /dev/null +++ b/docs/files/builders/trakt/userlist.md @@ -0,0 +1,47 @@ +--- +hide: + - toc +--- +# Trakt UserList + +Finds every movie/show in the Trakt UserList. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ warning "Trakt Configuration" + + [Configuring Trakt](../../../config/trakt.md) in the config is required for any of these builders. + +| Attribute | Description & Values | +|:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `userlist` | **Description:** Which Trakt userlist to query
**Values:**
`watchlist`Trakt User's Watchlist
`favorites`Trakt User's Personal Favorite list
`watched`Trakt User's Personal Watched list
`collection`Trakt User's Personal Collection list
| +| `user` | **Description:** The User who's user lists you want to query.
**Default:** `me`
**Values:** Username of User or `me` for the authenticated user. | +| `sort_by` | **Description:** How to sort the results
**Default:** `rank`
**Values:** `rank`, `added`, `released`, `title` | + +### Example Trakt UserList Builder(s) + +```yaml +collections: + Trakt Watchlist: + trakt_userlist: + userlist: watchlist + user: me + sort_by: released + collection_order: custom + sync_mode: sync +``` + +You can use multiple charts in one Builder using a list. + +```yaml +collections: + Trakt Watchlist: + trakt_userlist: + - userlist: watched + user: me + - userlist: collection + user: me + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tvdb.md b/docs/files/builders/tvdb.md deleted file mode 100644 index 54b4555e7..000000000 --- a/docs/files/builders/tvdb.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -hide: - - toc ---- -# TVDb Builders - -You can find items using the features of [TheTVDb.com](https://www.thetvdb.com/) (TVDb). - -| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | -|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| -| [`tvdb_list`](#tvdb-list) | Finds every item in a [TVDb List](https://www.thetvdb.com/lists) or [TVDb UserList](https://www.thetvdb.com/lists/custom) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | -| [`tvdb_show`](#tvdb-show) | Finds the series specified | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | -| [`tvdb_movie`](#tvdb-movie) | Finds the movie specified | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | - -=== "TVDb List" - - Finds every item in a [TVDb List](https://www.thetvdb.com/lists) or [TVDb UserList](https://www.thetvdb.com/lists/custom) - - The expected input is a TVDb List URL or TVDb UserList URL. Multiple values are supported as either a list or a - comma-separated string. - - The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated - and in a specific order. - - ???+ tip "Details Builder" - - You can replace `tvdb_list` with `tvdb_list_details` if you would like to fetch and use the description from the list - - ### Example TVDb List Builder(s) - - ```yaml - collections: - Arrowverse: - tvdb_list: https://www.thetvdb.com/lists/arrowverse - collection_order: custom - sync_mode: sync - ``` - ```yaml - collections: - Saved by the Bell: - tvdb_list: https://www.thetvdb.com/lists/6957 - collection_order: custom - sync_mode: sync - ``` - - * You can update the collection details with the TVDb list's description and poster by using `tvdb_list_details`. - * You can specify multiple lists in `tvdb_list_details` but it will only use the first one to update the collection details. - - ```yaml - collections: - Arrowverse: - tvdb_list_details: https://www.thetvdb.com/lists/arrowverse - collection_order: custom - sync_mode: sync - ``` - -=== "TVDb Show" - - Finds the show specified - - The expected input is a TVDb Series ID or TVDb Series URL. Multiple values are supported as either a list or a - comma-separated string. - - ???+ tip "Details Builder" - - You can replace `tvdb_show` with `tvdb_show_details` if you would like to fetch and use the description from the list - - ### Example TVDb Show Builder(s) - - ```yaml - collections: - Star Wars (Animated Shows): - tvdb_show: 83268, 283468 - ``` - ```yaml - collections: - Star Wars (Animated Shows): - tvdb_show: - - https://www.thetvdb.com/series/star-wars-the-clone-wars - - https://www.thetvdb.com/series/star-wars-rebels - ``` - - * You can update the collection details with the TVDb show's summary, poster, and background by using `tvdb_show_details`. - * You can specify multiple shows in `tvdb_show_details` but it will only use the first one to update the collection details. - * Posters and background in the library's asset directory will be used over the collection details unless `tvdb_poster`/`tvdb_background` is also specified. - - ```yaml - collections: - Star Wars (Animated Shows): - tvdb_show_details: 83268, 283468 - ``` - -=== "TVDb Movie" - - Finds the movie specified - - The expected input is a TVDb Movie ID or TVDb Movie URL. Multiple values are supported as either a list or a - comma-separated string. - - ???+ tip "Details Builder" - - You can replace `tvdb_movie` with `tvdb_movie_details` if you would like to fetch and use the description from the list - - ### Example TVDb Movie Builder(s) - - ```yaml - collections: - The Lord of the Rings: - tvdb_movie: 107, 157, 74 - ``` - ```yaml - collections: - The Lord of the Rings: - tvdb_movie: - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-fellowship-of-the-ring - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-two-towers - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-return-of-the-king - ``` - - * You can update the collection details with the TVDb movie's summary, poster, and background by using `tvdb_movie_details`. - * You can specify multiple movies in `tvdb_movie_details` but it will only use the first one to update the collection details. - * Posters and background in the library's asset directory will be used over the collection details unless `tvdb_poster`/`tvdb_background` is also specified. - - ```yaml - collections: - The Lord of the Rings: - tvdb_movie_details: - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-fellowship-of-the-ring - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-two-towers - - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-return-of-the-king - ``` \ No newline at end of file diff --git a/docs/files/builders/tvdb/list.md b/docs/files/builders/tvdb/list.md new file mode 100644 index 000000000..9a8e70e29 --- /dev/null +++ b/docs/files/builders/tvdb/list.md @@ -0,0 +1,45 @@ +--- +hide: + - toc +--- +# TVDb List + +Finds every item in a [TVDb List](https://www.thetvdb.com/lists) or [TVDb UserList](https://www.thetvdb.com/lists/custom) + +The expected input is a TVDb List URL or TVDb UserList URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a +comma-separated string. + +The `sync_mode: sync` and `collection_order: custom` Setting are recommended since the lists are continuously updated +and in a specific order. + +???+ tip "Details Builder" + + You can replace `tvdb_list` with `tvdb_list_details` if you would like to fetch and use the description from the list + +### Example TVDb List Builder(s) + +```yaml +collections: + Arrowverse: + tvdb_list: https://www.thetvdb.com/lists/arrowverse + collection_order: custom + sync_mode: sync +``` +```yaml +collections: + Saved by the Bell: + tvdb_list: https://www.thetvdb.com/lists/6957 + collection_order: custom + sync_mode: sync +``` + +* You can update the collection details with the TVDb list's description and poster by using `tvdb_list_details`. +* You can specify multiple lists in `tvdb_list_details` but it will only use the first one to update the collection details. + +```yaml +collections: + Arrowverse: + tvdb_list_details: https://www.thetvdb.com/lists/arrowverse + collection_order: custom + sync_mode: sync +``` diff --git a/docs/files/builders/tvdb/movie.md b/docs/files/builders/tvdb/movie.md new file mode 100644 index 000000000..86a51b473 --- /dev/null +++ b/docs/files/builders/tvdb/movie.md @@ -0,0 +1,43 @@ +--- +hide: + - toc +--- +# TVDb Movie + +Finds the movie specified + +The expected input is a TVDb Movie ID or TVDb Movie URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a +comma-separated string. + +???+ tip "Details Builder" + + You can replace `tvdb_movie` with `tvdb_movie_details` if you would like to fetch and use the description from the list + +### Example TVDb Movie Builder(s) + +```yaml +collections: + The Lord of the Rings: + tvdb_movie: 107, 157, 74 +``` +```yaml +collections: + The Lord of the Rings: + tvdb_movie: + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-fellowship-of-the-ring + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-two-towers + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-return-of-the-king +``` + +* You can update the collection details with the TVDb movie's summary, poster, and background by using `tvdb_movie_details`. +* You can specify multiple movies in `tvdb_movie_details` but it will only use the first one to update the collection details. +* Posters and background in the library's asset directory will be used over the collection details unless `tvdb_poster`/`tvdb_background` is also specified. + +```yaml +collections: + The Lord of the Rings: + tvdb_movie_details: + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-fellowship-of-the-ring + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-two-towers + - https://www.thetvdb.com/movies/the-lord-of-the-rings-the-return-of-the-king +``` \ No newline at end of file diff --git a/docs/files/builders/tvdb/overview.md b/docs/files/builders/tvdb/overview.md new file mode 100644 index 000000000..37691bc33 --- /dev/null +++ b/docs/files/builders/tvdb/overview.md @@ -0,0 +1,13 @@ +--- +hide: + - toc +--- +# TVDb Builders + +You can find items using the features of [TheTVDb.com](https://www.thetvdb.com/) (TVDb). + +| Builder | Description | Works with Movies | Works with Shows | Works with Playlists and Custom Sort | +|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------:|:------------------------------------------:|:------------------------------------------:| +| [`tvdb_list`](list.md) | Finds every item in a [TVDb List](https://www.thetvdb.com/lists) or [TVDb UserList](https://www.thetvdb.com/lists/custom) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | +| [`tvdb_show`](show.md) | Finds the series specified | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | +| [`tvdb_movie`](movie.md) | Finds the movie specified | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | \ No newline at end of file diff --git a/docs/files/builders/tvdb/show.md b/docs/files/builders/tvdb/show.md new file mode 100644 index 000000000..a12f879a1 --- /dev/null +++ b/docs/files/builders/tvdb/show.md @@ -0,0 +1,39 @@ +--- +hide: + - toc +--- +# TVDb Show + +Finds the show specified + +The expected input is a TVDb Series ID or TVDb Series URL. Multiple values are supported as either a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a +comma-separated string. + +???+ tip "Details Builder" + + You can replace `tvdb_show` with `tvdb_show_details` if you would like to fetch and use the description from the list + +### Example TVDb Show Builder(s) + +```yaml +collections: + Star Wars (Animated Shows): + tvdb_show: 83268, 283468 +``` +```yaml +collections: + Star Wars (Animated Shows): + tvdb_show: + - https://www.thetvdb.com/series/star-wars-the-clone-wars + - https://www.thetvdb.com/series/star-wars-rebels +``` + +* You can update the collection details with the TVDb show's summary, poster, and background by using `tvdb_show_details`. +* You can specify multiple shows in `tvdb_show_details` but it will only use the first one to update the collection details. +* Posters and background in the library's asset directory will be used over the collection details unless `tvdb_poster`/`tvdb_background` is also specified. + +```yaml +collections: + Star Wars (Animated Shows): + tvdb_show_details: 83268, 283468 +``` diff --git a/docs/files/dynamic.md b/docs/files/dynamic.md index e6ce971ab..3469d34c4 100644 --- a/docs/files/dynamic.md +++ b/docs/files/dynamic.md @@ -162,7 +162,7 @@ by this dynamic collection. **Attribute:** `exclude` - **Accepted Values:** List of keys + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keys ???+ example "Example" @@ -188,8 +188,8 @@ by this dynamic collection. **Attribute:** `addons` - **Accepted Values:** [Dictionary](../kometa/yaml.md#dictionaries) where the key is the `dynamic key` and the value is a - list of `dynamic keys` to combine. + **Accepted Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } where the key is the `dynamic key` and the value is a + list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of `dynamic keys` to combine. ???+ example "Example" @@ -213,7 +213,7 @@ by this dynamic collection. Each template is passed a few Template Variables you can use. - * `value`: The list of keys and addons + * `value`: The list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keys and addons * `key`: The dynamic key @@ -223,7 +223,7 @@ by this dynamic collection. **Attribute:** `template` - **Accepted Values:** Name of template or list of templates to use + **Accepted Values:** Name of template or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of templates to use ???+ example "Example" @@ -262,8 +262,8 @@ by this dynamic collection. **Attribute:** `template_variables` - **Accepted Values:** [Dictionary](../kometa/yaml.md#dictionaries) where the key is the Template Variable and the value - is another [Dictionary](../kometa/yaml.md#dictionaries) where the key is the `dynamic key` of the collection you want + **Accepted Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } where the key is the Template Variable and the value + is another Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } where the key is the `dynamic key` of the collection you want change the Template Variable for and the value is the new value for the Template Variable. ???+ example "Example" @@ -302,7 +302,7 @@ by this dynamic collection. **Attribute:** `remove_suffix` - **Accepted Values:** List or comma-separated string of suffixes to remove + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of suffixes to remove ???+ example "Example" @@ -323,7 +323,7 @@ by this dynamic collection. **Attribute:** `remove_prefix` - **Accepted Values:** List or comma-separated string of prefixes to remove + **Accepted Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of prefixes to remove ???+ example "Example" @@ -376,7 +376,7 @@ by this dynamic collection. **Attribute:** `key_name_override` - **Accepted Values:** [Dictionary](../kometa/yaml.md#dictionaries) where the key is the key name you want to change and + **Accepted Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } where the key is the key name you want to change and the value is what to change that key name to. ???+ example "Example" @@ -404,7 +404,7 @@ by this dynamic collection. **Attribute:** `title_override` - **Accepted Values:** [Dictionary](../kometa/yaml.md#dictionaries) where the key is the `dynamic key` you want to change + **Accepted Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } where the key is the `dynamic key` you want to change and the value is what to change the title to. ???+ example "Example" @@ -566,21 +566,21 @@ by this dynamic collection. Each template is passed a few Template Variables you can use. - * `value`: The list of keys and addons + * `value`: The list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keys and addons * `key`: The dynamic key * `key_name`: The key after `key_name_override`, `remove_prefix`, or `remove_suffix` are run on it. - * `included_keys`: The list of included keys + * `included_keys`: The list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of included keys - * `used_keys`: The list of all keys used (included_keys and their addon keys) + * `used_keys`: The list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of all keys used (included_keys and their addon keys)
**Attribute:** `other_template` - **Accepted Values:** Name of template or list of templates to use for the other collection only + **Accepted Values:** Name of template or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of templates to use for the other collection only ???+ example "Example" diff --git a/docs/files/dynamic_types.md b/docs/files/dynamic_types.md index ec3cef6bd..8ef6ab516 100644 --- a/docs/files/dynamic_types.md +++ b/docs/files/dynamic_types.md @@ -166,7 +166,7 @@ requirements of creating the collection. **`type` Value:** `imdb_awards` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`event_id` - Determines the [IMDb Event](https://www.imdb.com/event/) used." @@ -242,13 +242,13 @@ requirements of creating the collection. **`type` Value:** `letterboxd_user_lists` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`username` - Determines the Usernames to scan for lists."
This determines which Usernames are scanned. - **Allowed Values:** Username or list of Usernames + **Allowed Values:** Username or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Usernames ??? blank "`sort_by` - Determines the sort that the lists are returned." @@ -305,7 +305,7 @@ requirements of creating the collection. **`type` Value:** `trakt_user_lists` - **`data` Value:** List of Trakt Users (Use `me` to reference the authenticated user) + **`data` Value:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt Users (Use `me` to reference the authenticated user) **Valid Library Types:** Movies and Shows @@ -383,7 +383,7 @@ requirements of creating the collection. **`type` Value:** `trakt_people_list` - **`data` Value:** List of Trakt URLs + **`data` Value:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt URLs **Valid Library Types:** Movies and Shows @@ -421,7 +421,7 @@ requirements of creating the collection. **`type` Value:** `actor` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`depth` - Determines how many "top" acting credits per item." @@ -504,7 +504,7 @@ requirements of creating the collection. **`type` Value:** `director` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`depth` - Determines how many "top" directing credits per item." @@ -587,7 +587,7 @@ requirements of creating the collection. **`type` Value:** `writer` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`depth` - Determines how many "top" writing credits per item." @@ -670,7 +670,7 @@ requirements of creating the collection. **`type` Value:** `producer` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`depth` - Determines how many "top" producing credits per item." @@ -862,7 +862,7 @@ requirements of creating the collection. type: content_rating ``` -??? blank "`year` - Collections based on content ratings." +??? blank "`year` - Collections based on years."
Creates collections for each year found in the library. @@ -901,7 +901,7 @@ requirements of creating the collection. type: year ``` -??? blank "`episode_year` - Collections based on content ratings." +??? blank "`episode_year` - Collections based on episode year."
Creates collections for each year associated with episodes found in the library. @@ -1474,7 +1474,7 @@ requirements of creating the collection. **`type` Value:** `number` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Attributes ??? blank "`starting` - Determines the starting number." @@ -1550,7 +1550,7 @@ requirements of creating the collection. **`type` Value:** `custom` - **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) with the keys being the `dynamic key` and the values + **`data` Value:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } with the keys being the `dynamic key` and the values being the `key name` **Valid Library Types:** Movies, Shows, Music, and Video @@ -1587,7 +1587,7 @@ requirements of creating the collection. bet: BET+ itvx: ITVX disney: Disney+ - max: Max + hbo-max: HBO Max hulu: Hulu netflix: Netflix now: NOW diff --git a/docs/files/filters.md b/docs/files/filters.md index 322bd5bd7..1af86c847 100644 --- a/docs/files/filters.md +++ b/docs/files/filters.md @@ -59,7 +59,7 @@ tags: # Filters -Filters allow for you to filter every item added to the collection/overlay/playlist from every Builder using the `filters` attribute. +Filters allow for you to filter every item added to the collection/overlay/playlist from every Builder using the `filters` attribute. ## Using Filters @@ -75,7 +75,7 @@ filters: Anything that doesn't have both the Genre `Action` and the Country `Germany` will be ignored. -Multiple Filter Sets can be given as a list. With multiple sets only one of the sets must pass for the item to not be ignored. +Multiple Filter Sets can be given as a list. With multiple sets only one of the sets must pass for the item to not be ignored. ```yaml filters: @@ -87,42 +87,34 @@ filters: Anything that doesn't have either both the Genre `Action` and the Country `Germany` or the Genre `Comedy` and the Country `France` will be ignored. -All filter options are listed below. +All filter options are listed below. To display items filtered out add `show_filtered: true` to the definition. To display items that make it through the filters add `show_unfiltered: true` to the definition. You can use the `plex_all: true` Builder to filter from your entire library. ???+ warning - - Filters can be very slow, particularly on larger libraries. Try to build or narrow your items using a [Smart Label Collection](builders/plex.md#smart-label), [Plex Search](builders/plex.md#plex-search) or another [Builder](overview.md) if possible. + + Filters can be very slow, particularly on larger libraries. Try to build or narrow your items using a [Smart Label Collection](../files/settings.md#smart-label-definitions), [Plex Search](../files/builders/plex/search.md) or another [Builder](overview.md) if possible. ## Filter Options === "Boolean Filters" - + **Modifiers:** No Modifier - + ### Boolean Filter Attributes - -
| Boolean Filters | Description | Allowed Media | | :-------------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | | `has_collection` | Matches every item that has or does not have a collection | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `has_dolby_vision`(1) | Matches every item that has or does not have a dolby vision | `Movies`
`Shows`(2)
`Seasons`(3)
`Episodes` | + | `has_dolby_vision` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-filters-boolean-1" } | Matches every item that has or does not have a dolby vision | `Movies`
`Shows` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-filters-boolean-1" }
`Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-filters-boolean-1" }
`Episodes` | | `has_edition` | Matches every item that has or does not have an edition | `Movies` | | `has_overlay` | Matches every item that has or does not have an overlay | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums` | | `has_stinger` | Matches every item that has a [media stinger](http://www.mediastinger.com/) (After/During Credits Scene) | `Movies` | -
- - 1. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 2. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 3. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - #### Examples - + ```yaml collections: Movies with Mediastingers: @@ -137,35 +129,27 @@ You can use the `plex_all: true` Builder to filter from your entire library. filters: has_edition: true ``` - + === "Date Filters" **Modifiers:** No Modifier, `.not`, `.before`, `.after`, or `.regex` - + Date filters can **NOT** take multiple values. - - ### Date Filter Attributes -
+ ### Date Filter Attributes | Date Filters | Description | Allowed Media | | :------------------------------- | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------- | | `added` | Uses the date added attribute to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `first_episode_aired`(1) | Uses the first episode aired date to match | `Shows` | - | `last_episode_aired_or_never`(3) | Similar to `last_episode_aired` but also includes those that haven't aired yet | `Shows` | - | `last_episode_aired`(2) | Uses the last episode aired date to match | `Shows` | + | `first_episode_aired` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-filters-date-1" } | Uses the first episode aired date to match | `Shows` | + | `last_episode_aired_or_never` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-filters-date-1" } | Similar to `last_episode_aired` but also includes those that haven't aired yet | `Shows` | + | `last_episode_aired` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-filters-date-1" } | Uses the last episode aired date to match | `Shows` | | `last_played` | Uses the date last played attribute to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | | `release` | Uses the release date attribute (originally available) to match | `Movies`
`Shows`
`Episodes`
`Albums` | -
- - 1. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 2. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 3. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - ???+ tip "Date Filter Modifiers" - + | Date Modifier | Description | Format | | :------------ | :-------------------------------------------------------------------- | :------------------------------------------------------------------------- | | No Modifier | Matches every item where the date attribute is in the last X days | **Format:** number of days
e.g. `30` | @@ -175,7 +159,7 @@ You can use the `plex_all: true` Builder to filter from your entire library. | `.regex` | Matches every item where the attribute matches the regex given | N/A | #### Examples - + ```yaml collections: Summer 2020 Movies: @@ -193,63 +177,32 @@ You can use the `plex_all: true` Builder to filter from your entire library. ``` === "Number Filters" - + **Modifiers:** No Modifier, `.not`, `.gt`, `.gte`, `.lt`, or `.lte` - + Number filters can **NOT** take multiple values. - + ### Number Filter Attributes - -
| Number Filters | Description | Allowed Media | | :--------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | - | `aspect`(15) | Uses the aspect attribute to match
minimum: `0.0` | `Movies`
`Shows`(16)
`Seasons`(17)
`Episodes` | - | `audience_rating` | Uses the audience rating attribute to match
`0.0` - `10.0` | `Movies`
`Shows`
`Episodes` | - | `channels`(6) | Uses the audio channels attribute to match
minimum: `0` | `Movies`
`Shows`(7)
`Seasons`(8)
`Episodes` | - | `critic_rating` | Uses the critic rating attribute to match
`0.0` - `10.0` | `Movies`
`Shows`
`Episodes`
`Albums` | - | `duration` | Uses the duration attribute to match using minutes
minimum: `0` | `Movies`
`Shows`
`Episodes`
`Tracks` | - | `height`(9) | Uses the height attribute to match
minimum: `0` | `Movies`
`Shows`(10)
`Seasons`(11)
`Episodes` | - | `plays` | Uses the plays attribute to match
minimum: `1` | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `stinger_rating`(23) | Uses the [Mediastinger](http://www.mediastinger.com/) rating to match.
The media stinger rating is if the after/during credits scene is worth staying for.
minimum: `0` | `Movies` | - | `tmdb_vote_average`(5) | Uses the tmdb vote average rating to match
minimum: `0.0` | `Movies`
`Shows` | - | `tmdb_vote_count`(4) | Uses the tmdb vote count to match
minimum: `1` | `Movies`
`Shows` | - | `tmdb_year`(2)(3) | Uses the year on TMDb to match
minimum: `1` | `Movies`
`Shows` | - | `user_rating` | Uses the user rating attribute to match
`0.0` - `10.0` | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `versions`(18) | Uses the number of versions found to match
minimum: `0` | `Movies`
`Shows`(19)
`Seasons`(20),
`Episodes`
`Artists`(21)
`Albums`(22)
`Tracks` | - | `width`(12) | Uses the width attribute to match
minimum: `0` | `Movies`
`Shows`(13)
`Seasons`(14)
`Episodes` | - | `year`(1) | Uses the year attribute to match
minimum: `1` | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Albums`
`Tracks` | - - -
- - 1. You can use `current_year` to have Kometa use the current year's value. This can be combined with a `-#` at the end to subtract that number of years. i.e. `current_year-2` - 2. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 3. You can use `current_year` to have Kometa use the current year's value. This can be combined with a `-#` at the end to subtract that number of years. i.e. `current_year-2` - 4. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 5. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 6. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 7. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 8. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 9. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 10. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 11. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 12. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 13. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 14. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 15. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 16. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 17. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 18. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 19. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 20. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 21. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 22. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 23. The actual numbers are pulled from the [Mediastingers](https://github.com/Kometa-Team/Mediastingers) Repo. - - + | aspect :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" } | Uses the aspect attribute to match
minimum: 0.0 | Movies
Shows :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Seasons :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Episodes | + | audience_rating | Uses the audience rating attribute to match
0.0 - 10.0 | Movies
Shows
Episodes | + | channels :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" } | Uses the audio channels attribute to match
minimum: 0 | Movies
Shows :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Seasons :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Episodes | + | critic_rating | Uses the critic rating attribute to match
0.0 - 10.0 | Movies
Shows
Episodes
Albums | + | duration | Uses the duration attribute to match using minutes
minimum: 0 | Movies
Shows
Episodes
Tracks | + | height :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" } | Uses the height attribute to match
minimum: 0 | Movies
Shows :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Seasons :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Episodes | + | plays | Uses the plays attribute to match
minimum: 1 | Movies
Shows
Seasons,
Episodes
Artists
Albums
Tracks | + | stinger_rating :material-numeric-6-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-4" } | Uses the [Mediastinger](http://www.mediastinger.com/) rating to match.
The media stinger rating is if the after/during credits scene is worth staying for.
minimum: 0 | Movies | + | tmdb_vote_average :material-numeric-4-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-2" } | Uses the tmdb vote average rating to match
minimum: 0.0 | Movies
Shows | + | tmdb_vote_count :material-numeric-4-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-2" } | Uses the tmdb vote count to match
minimum: 1 | Movies
Shows | + | tmdb_year :material-numeric-4-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-2" } :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-1" } | Uses the year on TMDb to match
minimum: 1 | Movies
Shows | + | user_rating | Uses the user rating attribute to match
0.0 - 10.0 | Movies
Shows
Seasons,
Episodes
Artists
Albums
Tracks | + | versions :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" } | Uses the number of versions found to match
minimum: 0 | Movies
Shows :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Seasons :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" },
Episodes
Artists :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Albums :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Tracks | + | width :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" } | Uses the width attribute to match
minimum: 0 | Movies
Shows :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Seasons :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-3" }
Episodes | + | year :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-1" } | Uses the year attribute to match
minimum: 1 | Movies
Shows
Seasons,
Episodes
Albums
Tracks | ???+ tip "Number Filter Modifiers" - + | Number Modifier | Description | Format | | :-------------- | :----------------------------------------------------------------------------------------- | :------------------------------------------------ | | No Modifier | Matches every item where the number attribute is equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | @@ -258,9 +211,9 @@ You can use the `plex_all: true` Builder to filter from your entire library. | `.lt` | Matches every item where the number attribute is less than the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | | `.lte` | Matches every item where the number attribute is less than or equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | | `.not` | Matches every item where the number attribute is not equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | - + #### Examples - + ```yaml collections: 9.0 Movies: @@ -283,63 +236,32 @@ You can use the `plex_all: true` Builder to filter from your entire library. === "String Filters" **Modifiers:** No Modifier, `.not`, `.is`, `.isnot`, `.begins`, `.ends`, or `.regex` - + String filters can take multiple values **only as a list**. ### String Filter Attributes -
- | String Filter | Description | Allowed Media | | :--------------------- | :--------------------------------------- | :-------------------------------------------------------------------------------------- | - | `audio_codec`(20) | Uses the audio codec tags to match | `Movies`
`Shows`(21)
`Seasons`(22)
`Episodes` | - | `audio_profile`(23) | Uses the audio profile tags to match | `Movies`
`Shows`(24)
`Seasons`(25)
`Episodes` | - | `audio_track_title`(9) | Uses the audio track titles to match | `Movies`
`Shows`(10)
`Seasons`(11)
`Episodes`
`Artists`(12)
`Albums`(13)
`Tracks` | - | `edition` | Uses the edition attribute to match | `Movies` | - | `filepath`(4) | Uses the item's filepath to match | `Movies`
`Shows`(5)
`Seasons`(6)
`Episodes`
`Artists`(7)
`Albums`(8)
`Tracks` | - | `folder` | Uses the item's folder to match | `Shows`
`Artists` | - | `record_label` | Uses the record label attribute to match | `Albums` | - | `studio` | Uses the studio attribute to match | `Movies`
`Shows` | - | `summary` | Uses the summary attribute to match | `Movies`
`Shows`
`Seasons`
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `title` | Uses the title attribute to match | `Movies`
`Shows`
`Seasons`
`Episodes`
`Artists`
`Albums`
`Tracks` | - | `tmdb_title`(1) | Uses the title from TMDb to match | `Movies`
`Shows` | - | `tvdb_status`(3) | Uses the status from TVDb to match | `Shows` | - | `tvdb_title`(2) | Uses the title from TVDb to match | `Shows` | - | `video_codec`(14) | Uses the video codec tags to match | `Movies`
`Shows`(15)
`Seasons`(16)
`Episodes` | - | `video_profile`(17) | Uses the video profile tags to match | `Movies`
`Shows`(18)
`Seasons`(19)
`Episodes` | - - -
- - 1. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 2. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 3. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 4. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 5. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 6. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 7. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 8. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 9. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 10. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 11. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 12. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 13. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 14. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 15. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 16. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 17. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 18. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 19. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 20. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 21. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 22. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 23. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 24. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 25. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). + | audio_codec :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the audio codec tags to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes | + | audio_profile :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the audio profile tags to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes | + | audio_track_title :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the audio track titles to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes
Artists :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Albums :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Tracks | + | edition | Uses the edition attribute to match | Movies | + | filepath :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the item's filepath to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes
Artists :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Albums :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Tracks | + | folder | Uses the item's folder to match | Shows
Artists | + | record_label | Uses the record label attribute to match | Albums | + | studio | Uses the studio attribute to match | Movies
Shows | + | summary | Uses the summary attribute to match | Movies
Shows
Seasons
Episodes
Artists
Albums
Tracks | + | title | Uses the title attribute to match | Movies
Shows
Seasons
Episodes
Artists
Albums
Tracks | + | tmdb_title :material-numeric-7-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-1" } | Uses the title from TMDb to match | Movies
Shows | + | tvdb_status :material-numeric-7-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-1" } | Uses the status from TVDb to match | Shows | + | tvdb_title :material-numeric-7-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-1" } | Uses the title from TVDb to match | Shows | + | video_codec :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the video codec tags to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes | + | video_profile :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" } | Uses the video profile tags to match | Movies
Shows :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Seasons :material-numeric-8-circle:{ data-tooltip data-tooltip-id="tippy-filters-string-2" }
Episodes | ???+ tip "String Filter Modifiers" - + | String Modifier | Description | | :-------------- | :----------------------------------------------------------------------------- | | No Modifier | Matches every item where the attribute contains the given string | @@ -370,54 +292,34 @@ You can use the `plex_all: true` Builder to filter from your entire library. **Modifiers:** No Modifier, `.not`, `.regex`, `.count_lt`, `.count_lte`, `.count_gt`, or `.count_gte` - Tag filters can take multiple values as a **list or a comma-separated string**. - - ### Tag Filter Attributes + Tag filters can take multiple values as a **list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string**. -
+ ### Tag Filter Attributes | Tag Filters | Description | Allowed Media | | :--------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | | `actor` | Uses the actor tags to match | `Movies`
`Shows`
`Episodes` | - | `audio_language`(5) | Uses the audio language tags to match | `Movies`
`Shows`(6)
`Seasons`(7)
`Episodes` | + | `audio_language` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" } | Uses the audio language tags to match | `Movies`
`Shows` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" }
`Seasons` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" }
`Episodes` | | `collection` | Uses the collection tags to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | | `content_rating` | Uses the content rating tags to match | `Movies`
`Shows`
`Episodes` | | `country` | Uses the country tags to match | `Movies`
`Artists` | | `director` | Uses the director tags to match | `Movies`
`Episodes` | | `genre` | Uses the genre tags to match | `Movies`
`Shows`
`Artists`
`Albums` | - | `imdb_keyword`(15) | Uses the keywords from IMDb to match See [Special](#special-filters) for more attributes | `Movies`
`Shows` | + | `imdb_keyword` :material-numeric-1-box:{ data-tooltip data-tooltip-id="tippy-filters-tag-3" } | Uses the keywords from IMDb to match See [Special](#special-filters) for more attributes | `Movies`
`Shows` | | `label` | Uses the label tags to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Artists`
`Albums`
`Tracks` | | `network` | Uses the network tags to match | `Shows` | - | `origin_country`(13) | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | `Shows` | + | `origin_country` :material-numeric-1-box:{ data-tooltip data-tooltip-id="tippy-filters-tag-3" } | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | `Shows` | | `producer` | Uses the actor tags to match | `Movies`
`Episodes` | - | `resolution`(2) | Uses the resolution tag to match | `Movies`
`Shows`(3)
`Seasons`(4)
`Episodes` | - | `subtitle_language`(8) | Uses the subtitle language tags to match | `Movies`
`Shows`(9)
`Seasons`(10)
`Episodes` | - | `tmdb_genre`(11) | Uses the genres from TMDb to match | `Movies`
`Shows` | - | `tmdb_keyword`(12) | Uses the keywords from TMDb to match | `Movies`
`Shows` | - | `tvdb_genre`(14) | Uses the genres from TVDb to match | `Shows` | + | `resolution` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" } | Uses the resolution tag to match | `Movies`
`Shows` :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-filters-number-1" }
`Seasons` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" }
`Episodes` | + | `subtitle_language` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" } | Uses the subtitle language tags to match | `Movies`
`Shows` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" }
`Seasons` :material-numeric-10-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-2" }
`Episodes` | + | `tmdb_genre` :material-numeric-1-box:{ data-tooltip data-tooltip-id="tippy-filters-tag-3" } | Uses the genres from TMDb to match | `Movies`
`Shows` | + | `tmdb_keyword` :material-numeric-1-box:{ data-tooltip data-tooltip-id="tippy-filters-tag-3" } | Uses the keywords from TMDb to match | `Movies`
`Shows` | + | `tvdb_genre` :material-numeric-1-box:{ data-tooltip data-tooltip-id="tippy-filters-tag-3" } | Uses the genres from TVDb to match | `Shows` | | `writer` | Uses the writer tags to match | `Movies`
`Episodes` | - | `year`(1) | Uses the year tag to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Albums`
`Tracks` | - -
- - 1. You can use `current_year` to have Kometa use the current year's value. This can be combined with a `-#` at the end to subtract that number of years. i.e. `current_year-2` - 2. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 3. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 4. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 5. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 6. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 7. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 8. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 9. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 10. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 11. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 12. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 13. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 14. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 15. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. + | `year` :material-numeric-9-circle:{ data-tooltip data-tooltip-id="tippy-filters-tag-1" } | Uses the year tag to match | `Movies`
`Shows`
`Seasons`,
`Episodes`
`Albums`
`Tracks` | ???+ tip "Tag Filter Modifiers" - + | Tag Modifier | Description | | :----------- | :---------------------------------------------------------------------------------------- | | No Modifier | Matches every item where the attribute matches the given string | @@ -429,7 +331,7 @@ You can use the `plex_all: true` Builder to filter from your entire library. | `.regex` | Matches every item where one value of this attribute matches the regex. | #### Examples - + ```yaml collections: Daniel Craig only James Bonds: @@ -452,36 +354,23 @@ You can use the `plex_all: true` Builder to filter from your entire library. === "Special Filters" Special Filters each have their own set of rules for how they're used. - - ### Attribute -
+ ### Attribute | Special Filters | Description | Allowed Media | | :--------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | | `albums` | Uses the item's albums attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an item's albums that must match the sub-filter. | `Artists` | | `episodes` | Uses the item's episodes attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an item's episodes that must match the sub-filter. | `Shows`
`Seasons` | | `history` | Uses the release date attribute (originally available) to match dates throughout history
`day`: Match the Day and Month to Today's Date
`month`: Match the Month to Today's Date
`1-30`: Match the Day and Month to Today's Date or `1-30` days before | `Movies`
`Shows`
`Episodes`
`Albums` | - | `imdb_keyword`(7)(8) | Uses the keywords from IMDb to match
`keywords`: list of keywords to match
`minimum_votes`: minimum number of votes keywords must have
`minimum_relevant`: minimum number of relevant votes keywords must have
`minimum_percentage`: minimum percentage of relevant votes keywords must have | `Movies`
`Shows` | - | `original_language`(1)
`original_language.not`(2) | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | `Movies`
`Shows` | + | `imdb_keyword` :material-numeric-3-box:{ data-tooltip data-tooltip-id="tippy-filter-special-2" } :material-numeric-4-box:{ data-tooltip data-tooltip-id="tippy-filter-special-3" } | Uses the keywords from IMDb to match
`keywords`: list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of keywords to match
`minimum_votes`: minimum number of votes keywords must have
`minimum_relevant`: minimum number of relevant votes keywords must have
`minimum_percentage`: minimum percentage of relevant votes keywords must have | `Movies`
`Shows` | + | `original_language` :material-numeric-2-box:{ data-tooltip data-tooltip-id="tippy-filter-special-1" }
`original_language.not` :material-numeric-2-box:{ data-tooltip data-tooltip-id="tippy-filter-special-1" } | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | `Movies`
`Shows` | | `seasons` | Uses the item's seasons attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an item's seasons that must match the sub-filter. | `Shows` | - | `tmdb_status`(3)
`tmdb_status.not`(4) | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | `Shows` | - | `tmdb_type`(5)
`tmdb_type.not`(6) | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | `Shows` | + | `tmdb_status` :material-numeric-3-box:{ data-tooltip data-tooltip-id="tippy-filter-special-2" }
`tmdb_status.not` :material-numeric-3-box:{ data-tooltip data-tooltip-id="tippy-filter-special-2" } | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | `Shows` | + | `tmdb_type` :material-numeric-3-box:{ data-tooltip data-tooltip-id="tippy-filter-special-2" }
`tmdb_type.not` :material-numeric-3-box:{ data-tooltip data-tooltip-id="tippy-filter-special-2" } | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | `Shows` | | `tracks` | Uses the item's tracks attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an item's tracks that must match the sub-filter. | `Artists`
`Albums` | -
- - 1. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 2. Filters using the special `episodes`/`tracks` [filter](#special-filters) with the [default percent](settings.md). - 3. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 4. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 5. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 6. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 7. Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. - 8. Also is a Tag Filter and can use all of those modifiers. - #### Examples - + ```yaml collections: Shows That Finished Too Soon: diff --git a/docs/files/item_updates.md b/docs/files/item_updates.md index 912492ae9..a1c79491c 100644 --- a/docs/files/item_updates.md +++ b/docs/files/item_updates.md @@ -41,48 +41,32 @@ All the following attributes update various details of the metadata for every it **None of these updates work with Playlists or Overlays.** -
- -| Attribute | Description | Allowed Values (default in **bold**) | -|:------------------------------|:------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `item_analyze` | Runs Plex's Analyze Operation on every movie/show in the collection | `true`, **`false`** | -| `item_assets` | Checks your assets folders for assets of every movie/show in the collection | `true`, **`false`** | -| `item_audio_language` (1) | Changes the preferred audio language of every movie/show in the collection | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW | -| `item_credits_detection` (2) | Changes the enable credits detection of every movie/show in the collection | `default` (Library default), `disabled` (Disabled) | -| `item_delete_episodes` (3) | Changes the delete episodes setting of every show in the collection | `never` (Never), `day` (After a day), `week` (After a week), `month` (After a month), `refresh` (On next refresh) | -| `item_edition` | Replaces the edition of every movie in the collection | Edition name (e.g. `Extended`, `Director's Cut`) | -| `item_episode_ordering`(4) | Changes the episode ordering of every show in the collection | `default` (Library default), `tmdb_aired`, `tvdb_aired`, `tvdb_dvd`, `tvdb_absolute` | -| `item_episode_sorting`(5) | Changes the episode sorting of every show in the collection | `default` (Library default), `oldest`, `newest` | -| `item_genre.remove` | Removes existing genres from every movie/show in the collection | Comma-separated list of genres to remove | -| `item_genre.sync` | Matches the genres of every movie/show in the collection to the genres provided | Comma-separated list of genres to sync | -| `item_genre` | Appends new genres to every movie/show in the collection | Comma-separated list of genres to append | -| `item_keep_episodes`(6) | Changes the keep episodes setting of every show in the collection | `all`, `5_latest`, `3_latest`, `latest`, `past_3`, `past_7`, `past_30` | -| `item_label.remove` | Removes existing labels from every movie/show in the collection | Comma-separated list of labels to remove | -| `item_label.sync` | Matches the labels of every movie/show in the collection to the labels provided | Comma-separated list of labels to sync | -| `item_label` | Appends new labels to every movie/show in the collection | Comma-separated list of labels to append | -| `item_lock_background` | Locks or unlocks the background of every movie/show in the collection | `true`, `false`, or leave blank | -| `item_lock_poster` | Locks or unlocks the poster of every movie/show in the collection | `true`, `false`, or leave blank | -| `item_lock_title` | Locks or unlocks the title of every movie/show in the collection | `true`, `false`, or leave blank | -| `item_metadata_language`(7) | Changes the metadata language of every movie/show in the collection | `default`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-T` | -| `item_refresh_delay` | Amount of time to wait between each `item_refresh` of every movie/show in the collection | Number greater than `0`, e.g. **`0`** | -| `item_refresh` | Refreshes the metadata of every movie/show in the collection | `true`, **`false`** | -| `item_season_display`(8) | Changes the season display of every show in the collection | `default` (Library default), `show`, `hide` | -| `item_subtitle_language`(9) | Changes the preferred subtitle language of every movie/show in the collection | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW` | -| `item_subtitle_mode`(10) | Changes the auto-select subtitle mode of every movie/show in the collection | `default` (Account default), `manual` (Manually selected), `foreign` (Shown with foreign audio), `always` (Always enabled) | -| `item_tmdb_season_titles` | Changes the season titles of every show in the collection to match TMDb | `true`, **`false`** | -| `item_use_original_title`(11) | Changes the use original title of every movie/show in the collection | `default` (Library default), `no`, `yes` | -| `non_item_remove_label` | Matches every movie/show that has the given label and is not in the collection and removes the label | Comma-separated list of labels to remove | - -
- -1. Must be using the Plex Movie or Plex Series Agent -2. Must be using the Plex Movie or Plex Series Agent -3. Only works with Show libraries -4. Only works with Show libraries -5. Only works with Show libraries -6. Only works with Show libraries -7. Must be using the Plex Movie or Plex Series Agent -8. Only works with Show libraries -9. Must be using the Plex Movie or Plex Series Agent -10. Must be using the Plex Movie or Plex Series Agent -11. Must be using the Plex Movie or Plex Series Agent \ No newline at end of file +| Attribute | Description | Allowed Values (default in **bold**) | +|:----------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `item_analyze` | Runs Plex's Analyze Operation on every movie/show in the collection | `true`, **`false`** | +| `item_assets` | Checks your assets folders for assets of every movie/show in the collection | `true`, **`false`** | +| `item_audio_language` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the preferred audio language of every movie/show in the collection | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW | +| `item_credits_detection` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the enable credits detection of every movie/show in the collection | `default` (Library default), `disabled` (Disabled) | +| `item_delete_episodes` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-2" } | Changes the delete episodes setting of every show in the collection | `never` (Never), `day` (After a day), `week` (After a week), `month` (After a month), `refresh` (On next refresh) | +| `item_edition` | Replaces the edition of every movie in the collection | Edition name (e.g. `Extended`, `Director's Cut`) | +| `item_episode_ordering` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-2" } | Changes the episode ordering of every show in the collection | `default` (Library default), `tmdb_aired`, `tvdb_aired`, `tvdb_dvd`, `tvdb_absolute` | +| `item_episode_sorting` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-2" } | Changes the episode sorting of every show in the collection | `default` (Library default), `oldest`, `newest` | +| `item_genre.remove` | Removes existing genres from every movie/show in the collection | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of genres to remove | +| `item_genre.sync` | Matches the genres of every movie/show in the collection to the genres provided | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of genres to sync | +| `item_genre` | Appends new genres to every movie/show in the collection | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of genres to append | +| `item_keep_episodes` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-2" } | Changes the keep episodes setting of every show in the collection | `all`, `5_latest`, `3_latest`, `latest`, `past_3`, `past_7`, `past_30` | +| `item_label.remove` | Removes existing labels from every movie/show in the collection | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of labels to remove | +| `item_label.sync` | Matches the labels of every movie/show in the collection to the labels provided | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of labels to sync | +| `item_label` | Appends new labels to every movie/show in the collection | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of labels to append | +| `item_lock_background` | Locks or unlocks the background of every movie/show in the collection | `true`, `false`, or leave blank | +| `item_lock_poster` | Locks or unlocks the poster of every movie/show in the collection | `true`, `false`, or leave blank | +| `item_lock_title` | Locks or unlocks the title of every movie/show in the collection | `true`, `false`, or leave blank | +| `item_metadata_language` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the metadata language of every movie/show in the collection | `default`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-T` | +| `item_refresh_delay` | Amount of time to wait between each `item_refresh` of every movie/show in the collection | Number greater than `0`, e.g. **`0`** | +| `item_refresh` | Refreshes the metadata of every movie/show in the collection | `true`, **`false`** | +| `item_season_display` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-2" } | Changes the season display of every show in the collection | `default` (Library default), `show`, `hide` | +| `item_subtitle_language` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the preferred subtitle language of every movie/show in the collection | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW` | +| `item_subtitle_mode` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the auto-select subtitle mode of every movie/show in the collection | `default` (Account default), `manual` (Manually selected), `foreign` (Shown with foreign audio), `always` (Always enabled) | +| `item_tmdb_season_titles` | Changes the season titles of every show in the collection to match TMDb | `true`, **`false`** | +| `item_use_original_title` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-item-updates-1" } | Changes the use original title of every movie/show in the collection | `default` (Library default), `no`, `yes` | +| `non_item_remove_label` | Matches every movie/show that has the given label and is not in the collection and removes the label | Comma-separated list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of labels to remove | diff --git a/docs/files/metadata.md b/docs/files/metadata.md index 84a2a1b50..234934a21 100644 --- a/docs/files/metadata.md +++ b/docs/files/metadata.md @@ -240,37 +240,26 @@ The `match` attribute is used to match movies within Plex to that definition wit can match and edit multiple items. The available matching options are outlined below. === "Movies" - |
Attribute
| Description | - | :--------------------------------------- | :------------------------------------------------------------------------------------------------------------ | - | `blank_edition`**[3](#table-annotations)** | Only matches movies that have no Edition.
**Default:** `false`
**Values:** `true` or `false` | - | `edition_contains`**[3](#table-annotations)** | Only matches where the movie's Edition contains the given string.
Can be a list (only one needs to match). | - | `edition`**[3](#table-annotations)** | Only matches movies that exactly match the movie's Edition.
Can be a list (only one needs to match). | - | `mapping_id`**[2](#table-annotations)** | Only matches movies that have the given TMDb or IMDb ID. | - | `title`**[1](#table-annotations)** | Only matches movies that exactly match the movie's Title.
Can be a list (only one needs to match). | - | `year` | Only matches movies that were released in the given year. | + |
Attribute
| Description | + |:-----------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------| + | `blank_edition` :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-metadata-3" } | Only matches movies that have no Edition.
**Default:** false
**Values:** true or false | + | `edition_contains` :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-metadata-3" } | Only matches where the movie's Edition contains the given string.
Can be a list (only one needs to match). | + | `edition` :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-metadata-3" } | Only matches movies that exactly match the movie's Edition.
Can be a list (only one needs to match). | + | `mapping_id` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-metadata-2" } | Only matches movies that have the given TMDb or IMDb ID. | + | `title` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-metadata-1" } | Only matches movies that exactly match the movie's Title.
Can be a list (only one needs to match). | + | `year` | Only matches movies that were released in the given year. | === "TV Shows" - |
Attribute
| Allowed Values | - |:-----------------------------------------|:----------------------------------------------------------------------------------------------------| - | `mapping_id`**[2](#table-annotations)** | Only matches shows that have the given TVDb or IMDb ID. | - | `title`**[1](#table-annotations)** | Only matches shows that exactly match the show's Title.
Can be a list (only one needs to match). | - | `year` | Only matches shows that were released in the given year. | + |
Attribute
| Allowed Values | + |:-----------------------------------------|:----------------------------------------------------------------------------------------------------------------| + | `mapping_id` :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-metadata-2" } | Only matches shows that have the given TVDb or IMDb ID. | + | `title` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-metadata-1" } | Only matches shows that exactly match the show's Title.
Can be a list (only one needs to match). | + | `year` | Only matches shows that were released in the given year. | === "Music" |
Attribute
| Allowed Values | |:-----------------------------------------|:--------------------------------------------------------------------------------------------------------| - | `title`**[1](#table-annotations)** | Only matches artists that exactly match the artist's Title.
Can be a list (only one needs to match). | - -### Table Annotations - -**1** When `title` is not provided and the mapping name was not specified as an ID, the default behaviour is to use the -mapping name as `title` for matching. - -**2** When `mapping_id` is not provided and the mapping name was specified as an ID, the default behaviour is to use the -mapping name as `mapping_id` for matching. - -**3** When the server does not have a Plex Pass then the Edition Field is not accessible. In this scenario, Kometa will check -the movie's filepath for `{edition-...}` to determine what the edition is. + | `title` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-metadata-1" } | Only matches artists that exactly match the artist's Title.
Can be a list (only one needs to match). | ??? example "Matching Examples (click to expand)" === "Movies" @@ -465,39 +454,35 @@ The available attributes for editing movies are as follows | `episodes` | Attribute used to edit episode metadata. The mapping name is the episode number in that season, the title of the episode, or the Originally Available date in the format `MM/DD`. | `Seasons` | | `f1_season` | F1 Season Year to make the Show represent a Season of F1 Races. See [Formula 1 Metadata Guide](../kometa/guides/formula.md) for more information. | `Shows` | | `round_prefix` | Used only with `f1_season` to add the round as a prefix to the Season (Race) Titles i.e. `Australian Grand Prix` --> `01 - Australian Grand Prix`. | `Shows` | -| `run_definition` | Used to specify if this definition runs.
Multiple can be used for one definition as a list or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | `Movies`, `Shows`, `Artists` | +| `run_definition` | Used to specify if this definition runs.
Multiple can be used for one definition as a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | `Movies`, `Shows`, `Artists` | | `seasons` | Attribute used to edit season metadata. The mapping name is the season number (use 0 for specials) or the season name. | `Shows` | | `shorten_gp` | Used only with `f1_season` to shorten `Grand Prix` to `GP` in the Season (Race) Titles i.e. `Australian Grand Prix` --> `Australian GP`. | `Shows` | | `tmdb_show` | TMDb Show ID to use for metadata. Used when the Movie in your library is actually a miniseries on TMDb. (Example: [Halo 4: Forward Unto Dawn](https://www.themoviedb.org/tv/56295) or [IT](https://www.themoviedb.org/tv/19614)) **This is not used to say this movie is the given ID.** | `Movies` | | `tracks` | Attribute used to edit track metadata. The mapping name is the track number on that Album, or the title of the Track. | `Albums` | -| `update_episodes` | Used to specify if this definition's episodes metadata will update.
Multiple can be used for one definition as a list or comma separated string. One `false` will cause it to fail.
**Values:** `true`, `false` | `Shows` | -| `update_seasons` | Used to specify if this definition's seasons metadata will update.
Multiple can be used for one definition as a list or comma separated string. One `false` will cause it to fail.
**Values:** `true`, `false` | `Shows` | +| `update_episodes` | Used to specify if this definition's episodes metadata will update.
Multiple can be used for one definition as a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string. One `false` will cause it to fail.
**Values:** `true`, `false` | `Shows` | +| `update_seasons` | Used to specify if this definition's seasons metadata will update.
Multiple can be used for one definition as a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string. One `false` will cause it to fail.
**Values:** `true`, `false` | `Shows` | -1. If the server does not have a Plex Pass then the Edition Field is not accessible. In this case Kometa will check the -movies filepath for `{edition-MOVIES EDITION}` to determine what the edition is. ### General Attributes -| Attribute |
Allowed Values
| Item Types | -|:-----------------------|:---------------------------------------------------------------|:------------------------------------------------------------------------| -| `audience_rating` | Number to change Audience Rating. | `Movies`, `Shows`, `Episodes` | -| `content_rating` | Text to change Content Rating. | `Movies`, `Shows`, `Episodes` | -| `critic_rating` | Number to change Critic Rating. | `Movies`, `Shows`, `Episodes`, `Albums` | -| `disc` | Text to change Disc. | `Tracks` | -| `edition`**1** | Text to change Edition. | `Movies` | -| `original_artist` | Text to change Original Artist. | `Tracks` | -| `original_title` | Text to change Original Title. | `Movies`, `Shows` | -| `originally_available` | Date to change Originally Available.
**Format:** YYYY-MM-DD | `Movies`, `Shows`, `Episodes`, `Albums` | -| `record_label` | Text to change Record Label. | `Albums` | -| `sort_title` | Text to change Sort Title. | `Movies`, `Shows`, `Episodes`, `Artists`, `Albums`, `Tracks` | -| `studio` | Text to change Studio. | `Movies`, `Shows` | -| `summary` | Text to change Summary. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | -| `tagline` | Text to change Tagline. | `Movies`, `Shows` | -| `title` | Text to change Title. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Tracks` | -| `track` | Text to change Track. | `Tracks` | -| `user_rating` | Number to change User Rating. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | - -1. Requires Plex Pass +| Attribute |
Allowed Values
| Item Types | +|:-----------------------------------------------------------------------------------------|:---------------------------------------------------------------|:------------------------------------------------------------------------| +| `audience_rating` | Number to change Audience Rating. | `Movies`, `Shows`, `Episodes` | +| `content_rating` | Text to change Content Rating. | `Movies`, `Shows`, `Episodes` | +| `critic_rating` | Number to change Critic Rating. | `Movies`, `Shows`, `Episodes`, `Albums` | +| `disc` | Text to change Disc. | `Tracks` | +| `edition` :material-numeric-4-circle:{ data-tooltip data-tooltip-id="tippy-metadata-4" } | Text to change Edition. | `Movies` | +| `original_artist` | Text to change Original Artist. | `Tracks` | +| `original_title` | Text to change Original Title. | `Movies`, `Shows` | +| `originally_available` | Date to change Originally Available.
Format: YYYY-MM-DD | `Movies`, `Shows`, `Episodes`, `Albums` | +| `record_label` | Text to change Record Label. | `Albums` | +| `sort_title` | Text to change Sort Title. | `Movies`, `Shows`, `Episodes`, `Artists`, `Albums`, `Tracks` | +| `studio` | Text to change Studio. | `Movies`, `Shows` | +| `summary` | Text to change Summary. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | +| `tagline` | Text to change Tagline. | `Movies`, `Shows` | +| `title` | Text to change Title. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Tracks` | +| `track` | Text to change Track. | `Tracks` | +| `user_rating` | Number to change User Rating. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | ### Tag Attributes @@ -505,19 +490,19 @@ You can add `.remove` to any tag attribute to only remove those tags i.e. `genre You can add `.sync` to any tag attribute to sync all tags vs just appending the new ones i.e. `genre.sync`. -| Attribute |
Allowed Values
| Item Types | -| :--------------- | :------------------------------------------------------- | :---------------------------------------------------------------------- | -| `collection` | List or comma-separated text of each Collection Tag. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | -| `country` | List or comma-separated text of each Country Tag. | `Movies` | -| `country` | List or comma-separated text of each Country Tag. | `Artists` | -| `director` | List or comma-separated text of each Director Tag. | `Movies`, `Episodes` | -| `genre` | List or comma-separated text of each Genre Tag. | `Movies`, `Shows`, `Artists`, `Albums` | -| `label` | List or comma-separated text of each Label Tag. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | -| `mood` | List or comma-separated text of each Mood Tag. | `Artists`, `Albums`, `Tracks` | -| `producer` | List or comma-separated text of each Producer Tag. | `Movies` | -| `similar_artist` | List or comma-separated text of each Similar Artist Tag. | `Artists` | -| `style` | List or comma-separated text of each Style Tag. | `Artists`, `Albums` | -| `writer` | List or comma-separated text of each Writer Tag. | `Movies`, `Episodes` | +| Attribute |
Allowed Values
| Item Types | +| :--------------- |:---------------------------------------------------------------------------------------------------------------------------------------| :---------------------------------------------------------------------- | +| `collection` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Collection Tag. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | +| `country` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Country Tag. | `Movies` | +| `country` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Country Tag. | `Artists` | +| `director` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Director Tag. | `Movies`, `Episodes` | +| `genre` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Genre Tag. | `Movies`, `Shows`, `Artists`, `Albums` | +| `label` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Label Tag. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums`, `Tracks` | +| `mood` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Mood Tag. | `Artists`, `Albums`, `Tracks` | +| `producer` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Producer Tag. | `Movies` | +| `similar_artist` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Similar Artist Tag. | `Artists` | +| `style` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Style Tag. | `Artists`, `Albums` | +| `writer` | List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated text of each Writer Tag. | `Movies`, `Episodes` | ### Image Attributes @@ -525,24 +510,25 @@ You can add `.sync` to any tag attribute to sync all tags vs just appending the | :---------------- | :----------------------------------------------- | :------------------------------------------------------------ | | `file_background` | Path to image in the file system. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums` | | `file_poster` | Path to image in the file system. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums` | +| `file_logo` | Path to image in the file system. | `Movies`, `Shows`, `Seasons`, `Episodes` | | `url_background` | URL of image publicly available on the internet. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums` | | `url_poster` | URL of image publicly available on the internet. | `Movies`, `Shows`, `Seasons`, `Episodes`, `Artists`, `Albums` | +| `url_logo` | URL of image publicly available on the internet. | `Movies`, `Shows`, `Seasons`, `Episodes` | ### Advanced Attributes -|
Attribute
| Allowed Values |
Item Types
| -| :--------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------- | -| `album_sorting` |
`default`Library default
`manual`Manually selected
`foreign`Shown with foreign audio
`always`Always enabled
| `Artists` | -| `audio_language`**1** | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW` | `Shows`, `Seasons` | -| `credits_detection`**1** |
`default`Library default
`disabled`Disabled
| `Movies`, `Shows` | -| `delete_episodes` |
`never`Never
`day`After a day
`week`After a week
`refresh`On next refresh
| `Shows` | -| `episode_ordering` |
`default`Library default
`tmdb_aired`The Movie Database (Aired)
`tvdb_aired`TheTVDb (Aired)
`tvdb_dvd`TheTVDb (DVD)
`tvdb_absolute`TheTVDb (Absolute)
| `Shows` | -| `episode_sorting` |
`default`Library default
`oldest`Oldest first
`newest`Newest first
| `Shows` | -| `keep_episodes` |
`all`All episodes
`5_latest`5 latest episodes
`3_latest`3 latest episodes
`latest`Latest episodes
`past_3`Episodes added in the past 3 days
`past_7`Episodes added in the past 7 days
`past_30`Episodes added in the past 30 days
| `Shows` | -| `metadata_language`**1** | `default`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW` | `Movies`, `Shows` | -| `season_display` |
`default`Library default
`show`Show
`hide`Hide
| `Shows` | -| `subtitle_language`**1** | `default`, `en`, `ar-SA`, `ca-ES`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-AU`, `en-CA`, `en-GB`, `en-US`, `es-ES`, `es-MX`, `et-EE`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `lt-LT`, `lv-LV`, `nb-NO`, `nl-NL`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sv-SE`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-HK`, `zh-TW` | `Shows`, `Seasons` | -| `subtitle_mode`**1** |
`default`Account default
`no`No
`yes`Yes
| `Shows`, `Seasons` | -| `use_original_title`**1** |
`default`Library default
`no`No
`yes`Yes
| `Movies`, `Shows` | +|
Attribute
| Allowed Values |
Item Types
| +|:----------------------------------------------------------------------------------------------------| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------- | +| `album_sorting` |
`default`Library default
`manual`Manually selected
`foreign`Shown with foreign audio
`always`Always enabled
| `Artists` | +| `audio_language` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } | `default`, `en`, `ar-SA`, ... `zh-TW` | `Shows`, `Seasons` | +| `credits_detection` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } |
`default`Library default
`disabled`Disabled
| `Movies`, `Shows` | +| `delete_episodes` |
`never`Never
`day`After a day
`week`After a week
`refresh`On next refresh
| `Shows` | +| `episode_ordering` | ... | `Shows` | +| `episode_sorting` | ... | `Shows` | +| `keep_episodes` | ... | `Shows` | +| `metadata_language` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } | `default`, `ar-SA`, ... `zh-TW` | `Movies`, `Shows` | +| `season_display` | ... | `Shows` | +| `subtitle_language` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } | `default`, `en`, ... `zh-TW` | `Shows`, `Seasons` | +| `subtitle_mode` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } | ... | `Shows`, `Seasons` | +| `use_original_title` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-metadata-5" } | ... | `Movies`, `Shows` | -1. Must be using the **New Plex Movie Agent** or the **New Plex TV Agent** diff --git a/docs/files/overlays.md b/docs/files/overlays.md index bfe9756e9..6ac059b17 100644 --- a/docs/files/overlays.md +++ b/docs/files/overlays.md @@ -403,39 +403,37 @@ Each Special Text Variables has multiple modifiers that can be used to format th ##### Special Rating Text -| Variable | Description | Item Types | -|:-------------------------------|:----------------------------------------|:-------------------------------------------------------------------| -| `anidb_average_rating` | AniDB Average Rating | `Movies` or `Shows` | -| `anidb_rating` | AniDB Rating | `Movies` or `Shows` | -| `anidb_score_rating` | AniDB Score Rating | `Movies` or `Shows` | -| `audience_rating` | Plex Audience Rating | `Movies`, `Shows`, or `Episodes` | -| `critic_rating` | Plex Critic Rating | `Movies`, `Shows`, or `Episodes` | -| `imdb_rating` | IMDb Rating | `Movies`, `Shows`, `Seasons`**1**, or `Episodes` | -| `mal_rating` | MyAnimeList Rating | `Movies` or `Shows` | -| `mdb_average_rating` | MDBList Average Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_imdb_rating` | MDBList IMDb Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_letterboxd_rating` | MDBList Letterboxd Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_metacritic_rating` | MDBList Metacritic Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_metacriticuser_rating` | MDBList Metacritic User Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_myanimelist_rating` | MDBList MyAnimeList Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_rating` | MDBList Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_tmdb_rating` | MDBList TMDb Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_tomatoes_rating` | MDBList Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_tomatoesaudience_rating` | MDBList Rotten Tomatoes Audience Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `mdb_trakt_rating` | MDBList Trakt Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `omdb_imdb_rating` | OMDb IMDb Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `omdb_metascore_rating` | OMDb Metascore Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `omdb_tomatoes_rating` | OMDb Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `plex_imdb_rating` | Plex IMDb Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `plex_tmdb_rating` | Plex TMDb Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `plex_tomatoes_rating` | Plex Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons`**1** | -| `plex_tomatoesaudience_rating` | Plex Rotten Tomatoes Audience Rating | `Movies`, `Shows`, `Seasons`**1** | -| `tmdb_rating` | TMDb Rating | `Movies`, `Shows`, `Seasons`, or `Episodes` | -| `trakt_rating` | Trakt Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `trakt_user_rating` | Trakt User Rating | `Movies`, `Shows`, `Seasons`**1** or `Episodes`**1** | -| `user_rating` | Plex User Rating | `Movies`, `Shows`, `Seasons`, or `Episodes` | - -**1** These services do not store season or episode-level ratings, using these options will allow show ratings to be applied at the season or episode level. +| Variable | Description | Item Types | +|:-------------------------------|:----------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `anidb_average_rating` | AniDB Average Rating | `Movies` or `Shows` | +| `anidb_rating` | AniDB Rating | `Movies` or `Shows` | +| `anidb_score_rating` | AniDB Score Rating | `Movies` or `Shows` | +| `audience_rating` | Plex Audience Rating | `Movies`, `Shows`, or `Episodes` | +| `critic_rating` | Plex Critic Rating | `Movies`, `Shows`, or `Episodes` | +| `imdb_rating` | IMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" }, or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mal_rating` | MyAnimeList Rating | `Movies` or `Shows` | +| `mdb_average_rating` | MDBList Average Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_imdb_rating` | MDBList IMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_letterboxd_rating` | MDBList Letterboxd Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_metacritic_rating` | MDBList Metacritic Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_metacriticuser_rating` | MDBList Metacritic User Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_myanimelist_rating` | MDBList MyAnimeList Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_rating` | MDBList Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_tmdb_rating` | MDBList TMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_tomatoes_rating` | MDBList Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_tomatoesaudience_rating` | MDBList Rotten Tomatoes Audience Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `mdb_trakt_rating` | MDBList Trakt Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `omdb_imdb_rating` | OMDb IMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `omdb_metascore_rating` | OMDb Metascore Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `omdb_tomatoes_rating` | OMDb Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `plex_imdb_rating` | Plex IMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `plex_tmdb_rating` | Plex TMDb Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `plex_tomatoes_rating` | Plex Rotten Tomatoes Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `plex_tomatoesaudience_rating` | Plex Rotten Tomatoes Audience Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `tmdb_rating` | TMDb Rating | `Movies`, `Shows`, `Seasons`, or `Episodes` | +| `trakt_rating` | Trakt Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `trakt_user_rating` | Trakt User Rating | `Movies`, `Shows`, `Seasons` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } or `Episodes` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlays-1" } | +| `user_rating` | Plex User Rating | `Movies`, `Shows`, `Seasons`, or `Episodes` | ???+ tip "Special Rating Text Modifiers" diff --git a/docs/files/playlists.md b/docs/files/playlists.md index 1b429c2dc..63f6a441c 100644 --- a/docs/files/playlists.md +++ b/docs/files/playlists.md @@ -119,9 +119,9 @@ There are multiple types of attributes that can be utilized within a playlist: | Attribute | Description | Required | | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------: | | `delete_playlist` | Will delete this playlist for the users defined by sync_to_users.
**Options:** `true` or `false` | :fontawesome-solid-circle-xmark:{ .red } | -| `exclude_users` | Determine which Users will be excluded from having the playlist synced.
This will override the global [`playlist_excude_users` Setting](../config/settings.md).
**Options:** Comma-separated string or list of users, `all` for every user who has server access, or leave blank for just the server owner. | :fontawesome-solid-circle-xmark:{ .red } | -| `libraries` | Determine which libraries the playlist will be built from.
**Options:** Comma-separated string or list of library mapping names defined in the `libraries` attribute in the base of your [Configuration File](../config/overview.md). | :fontawesome-solid-circle-check:{ .green } | -| `sync_to_users` | Determine which Users have the playlist synced.
This will override the global [`playlist_sync_to_users` Setting](../config/settings.md).
**Options:** Comma-separated string or list of users, `all` for every user who has server access, or leave blank for just the server owner. | :fontawesome-solid-circle-xmark:{ .red } | +| `exclude_users` | Determine which Users will be excluded from having the playlist synced.
This will override the global [`playlist_excude_users` Setting](../config/settings.md).
**Options:** Comma-separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of users, `all` for every user who has server access, or leave blank for just the server owner. | :fontawesome-solid-circle-xmark:{ .red } | +| `libraries` | Determine which libraries the playlist will be built from.
**Options:** Comma-separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of library mapping names defined in the `libraries` attribute in the base of your [Configuration File](../config/overview.md). | :fontawesome-solid-circle-check:{ .green } | +| `sync_to_users` | Determine which Users have the playlist synced.
This will override the global [`playlist_sync_to_users` Setting](../config/settings.md).
**Options:** Comma-separated string or list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of users, `all` for every user who has server access, or leave blank for just the server owner. | :fontawesome-solid-circle-xmark:{ .red } | * Any defined playlist will be always be visible by The Plex Media Server owner, so it doesn't need to be defined within `sync_to_users`. diff --git a/docs/files/settings.md b/docs/files/settings.md index 6359d8b6e..6874b125e 100644 --- a/docs/files/settings.md +++ b/docs/files/settings.md @@ -53,14 +53,14 @@ All the following attributes serve various functions as how the definition funct | `build_collection` | **Description:** When set to false the collection won't be created but items can still be added to Radarr/Sonarr. Does not work for playlists.
**Default:** `true`
**Values:** `true` or `false` | | `builder_level` | **Description:** Make season, episode, album or track collections/overlays from `plex_all`, `plex_search`, `trakt_list`, or `imdb_list` Builders and Filters
**Values:**
`season`Collection contains seasons
`episode`Collection contains episodes
`album`Collection contains albums
`track`Collection contains tracks
| | `cache_builders` | **Description:** Caches the items found by the builders for a number of days. This is useful if you run the same configuration on multiple libraries/servers in one run just set the value to `1`.
**Default:** `0`
**Values:** number 0 or greater | -| `changes_webhooks` | **Description:** Used to specify a definition changes webhook for just this definition.
**Values:** List of webhooks | +| `changes_webhooks` | **Description:** Used to specify a definition changes webhook for just this definition.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of webhooks | | `default_percent` | **Description:** Used to declare the default percent for `episodes`, `seasons`, `tracks`, and `albums` [special filters](filters.md#special-filters). See [Example](#default-percent-example) below.
**Default:** `50`.
**Values:** Integer between 1 and 100 | | `delete_below_minimum` | **Description:** Deletes the definition if below the minimum.
**Default:** `delete_below_minimum` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | -| `delete_collections_named` | **Description:** Used to delete any collections in your plex named one of the given collections.
**Values:** List of Collection Names to delete | +| `delete_collections_named` | **Description:** Used to delete any collections in your plex named one of the given collections.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Collection Names to delete | | `delete_not_scheduled` | **Description:** Deletes the definition if its skipped because its not scheduled.
**Default:** `delete_not_scheduled` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | | `ignore_blank_results` | **Description:** Used to not have Errors resulting from blank results from builders.
**Default:** `false`
**Values:** `true` or `false` | -| `ignore_ids` | **Description:** definition level `ignore_ids` which is combined with the library and global `ignore_ids`.
**Default:** `ignore_ids` [settings value](../config/settings.md) in the Configuration File
**Values:** List or comma-separated String of TMDb/TVDb IDs | -| `ignore_imdb_ids` | **Description:** definition level `ignore_imdb_ids` which is combined with the library and global `ignore_imdb_ids`.
**Default:** `ignore_imdb_ids` [settings value](../config/settings.md) in the Configuration File
**Values:** List or comma-separated String of IMDb IDs | +| `ignore_ids` | **Description:** definition level `ignore_ids` which is combined with the library and global `ignore_ids`.
**Default:** `ignore_ids` [settings value](../config/settings.md) in the Configuration File
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated String of TMDb/TVDb IDs | +| `ignore_imdb_ids` | **Description:** definition level `ignore_imdb_ids` which is combined with the library and global `ignore_imdb_ids`.
**Default:** `ignore_imdb_ids` [settings value](../config/settings.md) in the Configuration File
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated String of IMDb IDs | | `limit` | **Description:** Used to specify the max number of items for the definition
**Values:** Number greater than 0 | | `minimum_items` | **Description:** Minimum items that must be found to add to a definition.
**Default:** `minimum_items` [settings value](../config/settings.md) in the Configuration File
**Values:** number greater than 0 | | `missing_only_released` | **Description:** definition Level `missing_only_released` toggle.
**Default:** `missing_only_released` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | @@ -69,10 +69,10 @@ All the following attributes serve various functions as how the definition funct | `only_filter_missing` | **Description:** definition Level `only_filter_missing` toggle.
**Default:** `only_filter_missing` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | | `only_run_on_create` | **Description:** Used to only run the collection definition if the collection doesn't already exist.
**Default:** `false`
**Values:** `true` or `false` | | `run_again` | **Description:** Used to try and add all the missing items to the definition again after the daily run.
**Default:** `false`
**Values:** `true` or `false` | -| `run_definition` | **Description:** Used to specify if this definition runs.
Multiple can be used for one definition as a list or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | +| `run_definition` | **Description:** Used to specify if this definition runs.
Multiple can be used for one definition as a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma separated string. One `false` or unmatched library type will cause it to fail.
**Values:** `movie`, `show`, `artist`, `true`, `false` | | `save_report` | **Description:** definition level `save_report` toggle.
**Default:** `save_report` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | | `schedule` | **Description:** Used to specify the schedule when this definition will run.
**Default:** `daily`
**Values:** [Any Schedule Option](../config/schedule.md) | -| `server_preroll` | **Description:** Used to set the `Movie pre-roll video` Text box in Plex under Settings -> Extras.
See the [Movie pre-roll video section on the Plex Extras Page](https://support.plex.tv/articles/202920803-extras/) for what the semicolons `;` and commas `,` mean to Plex.
You can run this with a [schedule](../config/schedule.md) to change the pre-rolls automatically.
See [Example](#server-preroll-example) below.
**Values:** Any String or a List of Strings/Nested List of Strings
\* When using a list the top level elements are separated by semicolons `;` and the nested lists are separated by commas `,`. | +| `server_preroll` | **Description:** Used to set the `Movie pre-roll video` Text box in Plex under Settings -> Extras.
See the [Movie pre-roll video section on the Plex Extras Page](https://support.plex.tv/articles/202920803-extras/) for what the semicolons `;` and commas `,` mean to Plex.
You can run this with a [schedule](../config/schedule.md) to change the pre-rolls automatically.
See [Example](#server-preroll-example) below.
**Values:** Any String or a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Strings/Nested List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Strings
\* When using a list the top level elements are separated by semicolons `;` and the nested lists are separated by commas `,`. | | `show_filtered` | **Description:** definition level `show_filtered` toggle.
**Default:** `show_filtered` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | | `show_missing` | **Description:** definition level `show_missing` toggle.
**Default:** `show_missing` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | | `show_unfiltered` | **Description:** definition level `show_unfiltered` toggle.
**Default:** `show_unfiltered` [settings value](../config/settings.md) in the Configuration File
**Values:** `true` or `false` | @@ -80,7 +80,7 @@ All the following attributes serve various functions as how the definition funct | `sync_missing_to_trakt_list` | **Description:** Used to also sync missing items to the Trakt List specified by `sync_to_trakt_list`.
**Default:** `false`
**Values:** `true` or `false` | | `sync_mode` | **Description:** Used to change how builders sync with this definition.
**Default:** `sync_mode` [settings value](../config/settings.md) in the Configuration File
**Values:** `sync` or `append`
See main [settings page](../config/settings.md#sync-mode) | | `sync_to_trakt_list` | **Description:** Used to specify a trakt list you want the definition synced to.
**Values:** Trakt List Slug you want to sync to | -| `template` | **Description:** Used to specify a template and Template Variables to use for this definition. See the [Templates Page](templates.md) for more information.
**Values:** Dictionary | +| `template` | **Description:** Used to specify a template and Template Variables to use for this definition. See the [Templates Page](templates.md) for more information.
**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } | | `test` | **Description:** When running in Test Mode (`--run-tests` [option](../kometa/environmental.md)) only definitions with `test: true` will be run.
**Default:** `false`
**Values:** `true` or `false` | | `tmdb_birthday` | **Description:** Controls if the Definition is run based on `tmdb_person`'s Birthday. Has 3 possible attributes `this_month`, `before` and `after`.
**Values:**
`this_month`Run's if Birthday is in current Month`true`/`false`
`before`Run if X Number of Days before the BirthdayNumber 0 or greater
`after`Run if X Number of Days after the BirthdayNumber 0 or greater
| | `tmdb_deathday` | **Description:** Controls if the Definition is run based on `tmdb_person`'s Deathday. Has 3 possible attributes `this_month`, `before` and `after`.
**Values:**
`this_month`Run's if Deathday is in current Month`true`/`false`
`before`Run if X Number of Days before the DeathdayNumber 0 or greater
`after`Run if X Number of Days after the DeathdayNumber 0 or greater
| @@ -89,12 +89,12 @@ All the following attributes serve various functions as how the definition funct ## Smart Label Definitions -Smart Labels are a process that Kometa uses to build [Smart Collections](builders/plex.md#understanding-smart-vs-manual-collections) using non-Plex builders. +Smart Labels are a process that Kometa uses to build [Smart Collections](builders/plex/overview.md#understanding-smart-vs-manual-collections) using non-Plex builders. Instead of building a Manual (also known as Dumb or non-Smart) Collection with items from third-party builders, Kometa applies a label to every item that is discovered by the builder. -This label is then used as part of a [Smart Filter Builder](builders/plex.md#smart-filter-builder) to create a Smart Collection showing any item that has that label. +This label is then used as part of a [Smart Filter Builder](builders/plex/smart-filter.md) to create a Smart Collection showing any item that has that label. The result is a Smart Collection which only updates when the specific label is added/removed from items within Plex (either by the user or Kometa adding or remove the label). @@ -146,7 +146,7 @@ but the Smart Filter now includes two new criteria; that the item must be unplay ### Smart Labels & Plex Collectionless Smart Label definitions are especially powerful because Smart Collections are not subject to the usual show/hide rules that affect Manual collections. -As such, it can help resolve issues like those described in [Plex Collectionless](builders/plex.md#plex-collectionless). +As such, it can help resolve issues like those described in [Plex Collectionless](builders/plex/collectionless). For example, if Marvel Cinematic Universe is set up using the smart label method, and all other Marvel-related collections are Manual collections, Plex will handle the visibility correctly across grouped collections. @@ -156,7 +156,7 @@ A good rule of thumb is: each item in your library should belong to no more than The only downside of using smart collections is that they are unable to be sorted by `custom` (which uses the order of the original builder). In order to have a custom ordered Marvel Cinematic Universe Collection with the show/hide of the collection to work correctly you will have to use -[Plex Collectionless](builders/plex.md#plex-collectionless). +[Plex Collectionless](builders/plex/collectionless). ## Default Percent Example diff --git a/docs/files/templates.md b/docs/files/templates.md index 0cb88bc80..d62d25e43 100644 --- a/docs/files/templates.md +++ b/docs/files/templates.md @@ -52,7 +52,7 @@ In addition, templates also have a few special attributes that they can use: ??? blank "`default` - Sets what Template Variables default to."
The `default` attribute allows default values for Template Variables which will be used if they're not - specified in the call. You need to provide a list of variables and the value that variable should get if not told differently when the template is referenced. See the tooltips below. + specified in the call. You need to provide a list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of variables and the value that variable should get if not told differently when the template is referenced. See the tooltips below. **A variable cannot be default if it is a conditional variable.** @@ -140,7 +140,7 @@ In addition, templates also have a few special attributes that they can use: | Attribute | Description | |:------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `conditions` | A list of condition blocks where if all conditions are met then the variable will be the `value` specified in that condition block. Once all conditions in a block are met, that `value` will be used and no other blocks will be run.
**This attribute is required** | + | `conditions` | A list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of condition blocks where if all conditions are met then the variable will be the `value` specified in that condition block. Once all conditions in a block are met, that `value` will be used and no other blocks will be run.
**This attribute is required** | | `default` | The default value for when no condition block is met. If default is not specified the variable becomes an optional variable.
**This attribute is optional** | #### Condition Blocks @@ -211,7 +211,7 @@ In addition, templates also have a few special attributes that they can use: ??? blank "`move_prefix` - List of prefixes to move to the end of the collection/playlist name for sorting." -
The `move_prefix` attribute can be used to specify a list or comma-separated string of +
The `move_prefix` attribute can be used to specify a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of prefixes to move to the end of the collection/playlist name for sorting. This changes the Template Variables `collection_sort`, `playlist_sort`, and `mapping_sort`. @@ -234,7 +234,7 @@ In addition, templates also have a few special attributes that they can use: name: Movies tmdb_id: 86311 Iron Man: - 1 template: + template: name: Movies tmdb_id: 131292 ``` diff --git a/docs/files/updates.md b/docs/files/updates.md index b92d30df4..1863a9b25 100644 --- a/docs/files/updates.md +++ b/docs/files/updates.md @@ -38,10 +38,10 @@ All the following attributes update various details of the definition's Metadata **Only `tmdb_person` works with Playlists.** | Attribute | Description & Values | -| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| :--------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `collection_filtering` | **Description:** Changes the Collection Filtering
**Smart Collections Only**
**Values:**
`admin`Always the server admin user
`user`User currently viewing the content
| | `collection_mode` | **Description:** Changes the Collection Mode
**Values:**
`default`Library default
`hide`Hide Collection
`hide_items`Hide Items in this Collection
`show_items`Show this Collection and its Items
| -| `collection_order` | **Description:** Changes the Collection Order
**Normal Collections Only**
When using `custom.asc`/`custom.desc` you can only have a single Builder in the collection.
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom.asc`/`custom.desc`Order Collection Via the Builder Order ascending or descending
[Any `plex_search` Sort Option](builders/plex.md#sort-options)Order Collection by any `plex_search` Sort Option
| +| `collection_order` | **Description:** Changes the Collection Order
**Normal Collections Only**
When using `custom.asc`/`custom.desc` you can only have a single Builder in the collection.
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom.asc`/`custom.desc`Order Collection Via the Builder Order ascending or descending
[Any `plex_search` Sort Option](builders/plex/sort-options.md)Order Collection by any `plex_search` Sort Option
| | `content_rating` | **Description:** Changes the content rating.
**Values:** Text to change Content Rating | | `file_theme` | **Description:** Changes the Collection Theme to the file location provided.
**Values:** Path to mp3 file | | `label.remove` | **Description:** Removes existing labels from the collection.
**Values:** Comma-separated string of labels to remove | @@ -49,7 +49,7 @@ All the following attributes update various details of the definition's Metadata | `label` | **Description:** Appends new labels.
**Values:** Comma-separated string of labels to append | | `sort_title` | **Description:** Changes the sort title.
You can "promote" certain collections to the top of a library by creating a sort title starting with a `+` or "demote" certain collections to the bottom of a library by creating a sort title starting with a `~`.
**Values:** Text to change Sort Title | | `tmdb_person_offset` | **Description:** Offsets which search results are used by `tmdb_person`.
**Values:** Any number greater than 0
**Default:** 0 | -| `tmdb_person` | **Description:** Changes summary and poster to a TMDb Person's biography and profile to the first specified person as well as allow the people specified to be used in [Plex Searches](builders/plex.md#plex-search).
**Values:** TMDb Person ID or Actor Name (Will pull the first ID from the TMDb search results) (List or Comma-separated string) | +| `tmdb_person` | **Description:** Changes summary and poster to a TMDb Person's biography and profile to the first specified person as well as allow the people specified to be used in [Plex Searches](builders/plex/search).
**Values:** TMDb Person ID or Actor Name (Will pull the first ID from the TMDb search results) (List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string) | | `url_theme` | **Description:** Changes the Collection Theme to the URL provided.
**Values:** URL to mp3 file | | `visible_home` | **Description:** Changes collection visible on Home Tab (Only works with Plex Pass)
**Values:**
`true`Visible
`false`Not Visible
[Any `schedule` Option](../config/schedule.md)Visible When Scheduled
| | `visible_library` | **Description:** Changes collection visible on Library Recommended Tab (Only works with Plex Pass)
**Values:**
`true`Visible
`false`Not Visible
[Any `schedule` Option](../config/schedule.md)Visible When Scheduled
| diff --git a/docs/index.md b/docs/index.md index eee566f30..12d9c877a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -210,7 +210,7 @@ hide: "![Movie Collection Preview](assets/images/movie-collections.png){ width=\"600\" }", "\"Show": "![Show Collection Preview](assets/images/show-overlays.png){ width=\"600\" }", - "(https://kometa.wiki/en/latest/": "(", "/)": ".md)" + "(https://kometa.wiki/en/latest/": "(", "/)": ")" }' rewrite-relative-urls=false %} diff --git a/docs/kometa/environmental.md b/docs/kometa/environmental.md index f4835d4ae..abc3a501a 100644 --- a/docs/kometa/environmental.md +++ b/docs/kometa/environmental.md @@ -582,8 +582,7 @@ Kometa will load those environment variables when it starts up, and you don't ha To preserve functionality of Kometa, this will **not** remove the Overlay label, which is required for Kometa to know which items have Overlays applied. - This will impact any [Smart Label Collections](../files/builders/plex.md#smart-label-builder) that you have in your library. - + This will impact any [Smart Label Collections](../files/settings/#smart-label-definitions) that you have in your library. We do not recommend using this on a regular basis if you also use any operations or collections that update labels, as you are effectively deleting and adding labels on each run. diff --git a/docs/kometa/guides/assets.md b/docs/kometa/guides/assets.md index fbff5fd5e..a26f0ca2e 100644 --- a/docs/kometa/guides/assets.md +++ b/docs/kometa/guides/assets.md @@ -12,7 +12,7 @@ The Image Asset Directories can be used to update the posters and backgrounds of It is a folder containing artwork (posters and/or backgrounds) that is *typically* entirely separate to your media directories. -The only connection an asset directory has with your media directories is that the name of the folder your movie or series is in is the "asset name". +The only connection an asset directory has with your media directories is that the name of the folder your movie or series is in is the "asset name". Kometa uses that "asset name" as the **key** to find the artwork in the asset directory. Media directory: @@ -40,13 +40,13 @@ OR without asset folders ^^^^^^^^^^^^^^^^ "asset name" used as lookup key ``` -You **can** use your media directories as the asset directories (if for example you like to keep your artwork next to your media files), but this is not typical, nor is it required. +You **can** use your media directories as the asset directories (if for example you like to keep your artwork next to your media files), but this is not typical, nor is it required. Kometa does not require direct access to your media directories in normal use, and it is typically run remote from the Plex server, which is why the asset directory is *typically* entirely separate. ## Requirements and configuration -If you want to apply artwork to movies and shows using the asset directory, the Kometa asset pipeline *requires* that your movies and shows are in folders of their own. +If you want to apply artwork to movies and shows using the asset directory, the Kometa asset pipeline *requires* that your movies and shows are in folders of their own. The name that Kometa will use to look up the asset poster for a movie is the folder that the movie file is located in *on disk*, and each movie/show needs to have a unique asset name. In other words, this works: @@ -69,7 +69,7 @@ If your movies and shows are not in individual folders, setting art using the as You can specify your asset folders under the `settings` attribute `asset_directory`: -???+ important +???+ important Assets can be stored anywhere on the host system that Kometa has visibility of (i.e. if using docker, the directory must be mounted/visible to the docker container). @@ -102,13 +102,14 @@ Assets can be applied to collections [managed or unmanaged], playlists, and medi Managed Collection and Playlist assets are applied whenever that collection/playlist is run. You do not have to specifically enable assets for these items; Kometa will always search for and apply them. - -Item [movie/show/etc] assets and Unmanaged Collections assets have to be specifically enabled before Kometa will search for and apply them. Do this by enabling the `assets_for_all` Library Operation: + +Item [movie/show/etc] assets and Unmanaged Collections assets have to be specifically enabled before Kometa will search for and apply them. Do this by enabling the `assets_for_all` and/or `assets_for_all_collections` Library Operations: ```yaml Movies: operations: assets_for_all: true + assets_for_all_collections: true ``` If you want to silence the `Asset Warning: No poster or background found in an assets folder for 'TITLE'` you can use the [`show_missing_assets` Setting Attribute](../../config/settings.md): @@ -120,18 +121,18 @@ settings: ## Asset interaction with overlays -If a media item has an asset associated with it, that asset image is taken as the source of truth for what artwork the item should have, -and the overlay pipeline will no longer download and back up the base artwork from Plex. Using the Asset Directory to assign custom art is +If a media item has an asset associated with it, that asset image is taken as the source of truth for what artwork the item should have, +and the overlay pipeline will no longer download and back up the base artwork from Plex. Using the Asset Directory to assign custom art is the simplest and safest way to ensure that the overlay pipeline doesn't unexpectedly overwrite your custom artwork in Plex. ## Asset Naming -The table below shows the asset folder path structures that will be searched for. There are two options for how Kometa looks at the files inside your Asset Directories. Choose an option with +The table below shows the asset folder path structures that will be searched for. There are two options for how Kometa looks at the files inside your Asset Directories. Choose an option with the [`asset_folders` Setting Attribute](../../config/settings.md). Note that `asset_folders` is a toggle; you can't put some images in folders and some not in a context where it is enabled. Assets can be stored anywhere on the host system that Kometa has visibility of (i.e. if using docker, the directory must be mounted/visible to the docker container). -???+ important +???+ important The below table assumes that your assets are stored within the directory mapped to `config` in your Kometa environment. @@ -141,10 +142,13 @@ Assets can be stored anywhere on the host system that Kometa has visibility of ( |:---------------------------------|:---------------------------------------------------------| | Collection/Movie/Show poster | `/ASSET_NAME/poster.ext` | | Collection/Movie/Show background | `/ASSET_NAME/background.ext` | + | Collection/Movie/Show logo | `/ASSET_NAME/logo.ext` | | Season poster | `/ASSET_NAME/Season##.ext` | | Season background | `/ASSET_NAME/Season##_background.ext` | + | Season logo | `/ASSET_NAME/Season##_logo.ext` | | Episode poster | `/ASSET_NAME/S##E##.ext` | | Episode background | `/ASSET_NAME/S##E##_background.ext` | + | Episode logo | `/ASSET_NAME/S##E##_logo.ext` | === "ASSET_FOLDERS=False" @@ -152,10 +156,13 @@ Assets can be stored anywhere on the host system that Kometa has visibility of ( |:---------------------------------|:---------------------------------------------------------| | Collection/Movie/Show poster | `/ASSET_NAME.ext` | | Collection/Movie/Show background | `/ASSET_NAME_background.ext` | + | Collection/Movie/Show logo | `/ASSET_NAME_logo.ext` | | Season poster | `/ASSET_NAME_Season##.ext` | | Season background | `/ASSET_NAME_Season##_background.ext` | + | Season logo | `/ASSET_NAME_Season##_logo.ext` | | Episode poster | `/ASSET_NAME_S##E##.ext` | | Episode background | `/ASSET_NAME_S##E##_background.ext` | + | Episode logo | `/ASSET_NAME_S##E##_logo.ext` | ## Determining the "Asset Name" @@ -237,7 +244,7 @@ Assets can be stored anywhere on the host system that Kometa has visibility of ( config/assets/The Expanse (2015) {tvdb-280619}.ext config/assets/The Expanse (2015) {tvdb-280619}_background.ext ``` - + ## Season and Episode numbers === "Seasons" @@ -293,7 +300,7 @@ Assets can be stored anywhere on the host system that Kometa has visibility of ( * Replace `.ext` with the image extension * When `asset_folders` is set to `true` movie/show folders can be nested inside other folders, but you must specify how deep you want to search because the more levels to search the longer it takes. - + * You can specify how deep you want to scan by using the [`asset_depth` Setting Attribute](../../config/settings.md). Here's an example config folder structure with an assets directory with `asset_folders` set to true and false. diff --git a/docs/kometa/guides/images/overlay_label.png b/docs/kometa/guides/images/overlay_label.png new file mode 100644 index 000000000..f594167cd Binary files /dev/null and b/docs/kometa/guides/images/overlay_label.png differ diff --git a/docs/kometa/guides/order.md b/docs/kometa/guides/order.md index 722daafff..e0f3ecd69 100644 --- a/docs/kometa/guides/order.md +++ b/docs/kometa/guides/order.md @@ -399,7 +399,7 @@ Manual collections can utilize the `sort_by` attribute to define the sorting. limit: 50 ``` -The Sort Orders available for "Manual" collections are outlined on the **[Manual Plex Builder](../../files/builders/plex.md)** page. +The Sort Orders available for "Manual" collections are outlined on the **[Plex Sort Options](../../files/builders/plex/sort-options.md)** page. ??? tip @@ -444,7 +444,7 @@ if you have a Smart Label collection, you can pass the `sort_by` value you want smart_label: audience_rating.desc ``` -The Sort Orders available for "Smart" collections are outlined on the **[Plex Builder](../../files/builders/plex.md)** page. +The Sort Orders available for "Smart" collections are outlined on the **[Plex Sort Options](../../files/builders/plex/sort-options.md)** page. ??? tip diff --git a/docs/kometa/guides/overlays.md b/docs/kometa/guides/overlays.md new file mode 100644 index 000000000..8887c93e5 --- /dev/null +++ b/docs/kometa/guides/overlays.md @@ -0,0 +1,261 @@ +--- +search: + boost: 2 +hide: + - toc +--- +# Overlays Guide + +Kometa can apply overlays to item posters [movies, shows, and seasons] and episode images. + +The how of doing this is covered on the [overlays files](../../files/overlays.md) page. + +There are also a number of [default overlays](../../defaults/overlays.md) that you can leverage. + +This article is intended as an overview of how Kometa handles and applies overlays. + +## Important Prerequisite Facts + +### Applying overlays to a subset of the library + +Overlays are computed and applied at the library level. There is no way to manipulate overlays in any way on a subset of a library. + +The current implementation is: + +1. Determine what items in the library get which overlays. +2. Make the library look like that, removing and applying overlaid images as needed. + +Again, there is no way to target a **subset** of the library with overlays. You cannot: + +1. Skip applying overlays to items that don't change +2. Apply overlays to just that one new movie +3. Apply your show overlays on Monday and your episode overlays on Tuesday +4. Any other thing you can imagine that involves manipulating overlays on any other level than the **entire library** + +### Modifying existing overlays + +There is no need to remove overlays prior to changing them. Kometa will automatically add and remove overlays to reflect your current config. For example, if you add the default `network` and `streaming` overlays and then decide that you don't want the `streaming` one after all, you need only remove it from the config and run Kometa. That streaming overlay will be removed on the next run. *There is no need to remove overlays first or force Kometa to reapply overlays for this purpose*. + +## NOTE + +Some things in this article are discussed *later in the article*. These things are indicated in ***bold italic***. + +As you make your way through the article, all should fall into place. + +## How do overlays work? + +1. Kometa processes your library against your defined overlays, and decides which of those overlays apply to which items in the library. We'll call this the "overlay list". +2. Kometa removes overlays from ***items which presently have overlays*** but no longer appear in the overlay list +3. Then, for each item in the overlay list: + + a. Kometa applies overlays as needed to the ***clean art*** for the item. "As needed" here means generally that the visual appearance of the overlay has changed because overlays have been added or removed, or a value that drives an overlay like `resolution` changed, the base art changed [in cases where Kometa knows this], etc. + + b. Kometa uploads that overlaid image to Plex. + + c. Kometa makes a ***backup of the clean art*** (if one does not already exist) + + d. Kometa sets an ***"Overlay" label on the item*** in Plex so it knows that overlays have been applied to the item + + e. Kometa caches a description of the overlays it just applied to the item to use next time in step 3a above. + +Let's discuss each step. + +### Step 1: which overlays apply to which items + +Kometa processes the overlays in your config in sequence, tracking which items meet the criteria for each. + +This ends up with something like this: + +```shell +| Overlay | Number | +| ================================================ | ====== | +| Overlay File (0) Oscars Best Picture | 20 | +| Overlay File (0) Oscars Best Director | 21 | +| Overlay File (0) Golden Globe Winner | 30 | +| Overlay File (0) Golden Globe Best Director | 17 | +| Overlay File (0) BAFTA Winners | 14 | +| Overlay File (0) Cannes Winners | 6 | +| Overlay File (0) Berlinale Winners | 0 | +| Overlay File (0) Venice Winners | 3 | +| Overlay File (0) Sundance Winners | 3 | +| Overlay File (0) Emmys Winner | 0 | +| Overlay File (0) Critics Choice Winners | 9 | +| Overlay File (0) Spirit Winners | 8 | +| Overlay File (0) Cesar Winners | 1 | +| Overlay File (0) IMDb Top 250 | 117 | +| Overlay File (0) Letterboxd Top 250 | 67 | +| Overlay File (0) Rotten Tomatoes Verified Hot | 47 | +| Overlay File (0) Rotten Tomatoes Certified Fresh | 450 | +| Overlay File (0) Metacritic Must See | 152 | +| Overlay File (0) Commonsense Selection | 52 | +| Overlay File (0) Razzies Winner | 0 | +``` + +That's a single overlay [the default `ribbon`], just for illustration. + +Let's discuss the numbers. + +This library contains 806 items: + +```shell +| Loading All Movies from Library: Movies +| Loaded 806 Movies +``` + +but you will note that those values in the table sum to 1017. + +This is because a couple hundred items qualify for more than one ribbon; for example there are surely movies that meet all these: + +``` +| Overlay File (0) Oscars Best Picture | 20 | +| Overlay File (0) Golden Globe Winner | 30 | +| Overlay File (0) IMDb Top 250 | 117 | +| Overlay File (0) Letterboxd Top 250 | 67 | +| Overlay File (0) Rotten Tomatoes Certified Fresh | 450 | +| Overlay File (0) Metacritic Must See | 152 | +``` + +These are weighted, and only one will end up being applied. + +### Step 2. remove overlays from items which no longer appear in the overlay list + +As mentioned above, when Kometa applies an overlay to an item, it applies a label ("Overlay") to that item in Plex. + +In this step, Kometa queries Plex for everything with that label, and if anything with the label is **not** found in the current overlay list, the art for that item is replaced by the ***clean backup art***. + +### Step 3. Overlay application loop + +Now Kometa is going to iterate through every item in the library, applying the required overlays to each item. + +Here's a flowchart detailing the steps performed for each item in the library when applying overlays: + +![overlay application flowchart](https://raw.githubusercontent.com/Kometa-Team/Cookbook/refs/heads/main/cookbook/overlay-application-workflow.png) + +#### 3a. Apply overlays as needed to the ***clean art*** for the item + +Kometa applies overlays to a item in the overlay list in any of the following scenarios: + +1. This item has never had overlays before +2. This item's overlays differ from what got applied last time +3. `reapply_overlays` is set to true + +In cases like the example above where some items qualify for more than one of a given "set" of overlays, the relative weights of the multiple overlays in each set [or queue] are evaluated to leave one "winner" from the set that gets applied. + +When applying overlays, Kometa takes a copy of the ***current clean art*** for the item, scales it to a standard size (1000x1500 or 1920x1080), then composites the various overlay components on top of it. This overlaid image is generated in the format and quality level you have specified in the config. + +This image also gets an internal tag identifying it as an overlaid image which was created by Kometa. This prevents Kometa from double-overlaying an image in the event that Plex provides an already-overlaid image as the "clean art". + +#### 3b. Kometa uploads that overlaid image to Plex + +Once the image is generated, Kometa uploads that image to Plex. Plex requires that an uploaded image be smaller than 10MB, so if this composited image is larger than that [which is unlikely], the upload will be aborted and this will be noted in the log. This is typically only a problem if you are attempting to use very large source images from an external source. + +Every time a new image is uploaded to Plex, whether or not that image is the same as one that's already there, that new image is added to the Plex metadata, which increases the size of that metadata. Over time, this can lead to significant bloat, so things like reapplying overlays should be avoided. + +#### 3c. Kometa makes a backup of the clean art (if one does not already exist) + +If you are not using the [asset directory](./assets.md) to manage your custom art, Kometa will download the current art from Plex and store it in: + +``` +config/overlays/LIBRARY_NAME Original Posters/ +``` + +The images are named with the internal rating key of the item in Plex. This is fairly opaque, but it is the simplest unambiguous way to link the clean art with the item to which it applies. + +This clean art is used when overlays are updated, removed, or reapplied. You should not mess with this directory or its contents. + +This backup is required since once Kometa uploads new art, it no longer has any idea what the art on the thing *was*. This clean backup copy is the only source of that information. + +If you *ARE* using the [asset directory](./assets.md) to manage your custom art, then that asset image becomes the source of truth for the clean art and the backup directory is not used. + +#### 3d. Kometa sets an ***"Overlay" label on the item*** in Plex so it knows that overlays have been applied to the item + +Once the image has been generated and uploaded, Kometa sets a sharing label on the item in Plex: "Overlay" + +![](images/overlay_label.png) + +Kometa uses this label as discussed earlier to determine which items already have overlays; this mainly affects where it should get the clean art [in the absence of a asset image]. Is there a label? Get the art from the backup directory. No label? Download the art from Plex. As above, if there is asset art for this item, the label is not used for this purpose since the asset image is the single source of truth. + +#### 3e. Kometa caches a description of the overlays it just applied to the item to use next time in step 3a above + +Last, when an overlay is successfully applied, Kometa caches [in its own database] a record indicating which overlays were applied. It uses this cache on each run to decide if the overlays on the current run differ from the last run, as discussed above in step 3a. + +## Overlay Success in Logs + +If overlay application succeeds, you will see one of two things in the log: + +Overlay was applied successfully on this run: + +``` + | Some Cool Movie +[INFO] | Overlays Applied: Overlay File (0) 1080P, Overlay File (5) WEB +``` + +Overlay was NOT applied on this run because it hasn't changed from last time: + +``` + | Some Cool Movie +[INFO] | Overlay Update Not Needed +``` + +## Common Overlay Issues + +The most common issue with overlays is that the item you expected an overlay on doesn't qualify for the overlay for some reason. With the defaults, a common reason is that you aren't using TRaSH naming for your files, and some of the default overlays expect it. + +The next most common is when the image sent from Plex is already overlaid: + +``` +| Overlay Error: This item's poster already has an Overlay. There is no Kometa setting to change; manual attention required. +``` + +The most common reason for *that* is a season which does not have artwork of its own, so Plex provides the already-overlaid show artwork when asked for the season artwork. + +The "manual attention" that is required is you going into the edit screen in Plex and choosing a poster that doesn't already have overlays on it, then rerunning Kometa. + +Less common: + +``` +| Overlay Error: cannot identify image file '/config/overlays/TV Shows Original Posters/18482.jpg' +``` + +The backup art that Plex provided and Kometa saved has gone corrupt. This is typically indicative of some disk issues. + +## So You Wanna... + +### So you wanna change the artwork on something after overlays are applied? + +Simplest: + +Put the new artwork into an [asset directory](./assets.md), make sure the `assets_for_all` operation is enabled on the library and `prioritize_assets` is set to `true`, and run Kometa. Kometa will pick up the new art, apply overlays to it, and send it to Plex. if you want to change the art, just drag a new image into that folder. + +Slightly less Simple: + +Change the art on the item in Plex, and remove the "Overlay" label from the item. If you have Plex Pass, you can remove the label from the item in the UI for Movies and Shows. You can't remove the label from seasons or episodes in the UI at all. You will need to use a script or Kometa itself to do that. + +??? "Removing the Overlay label from seasons (click to expand)" + + ```yaml + collections: + Remove Overlay Labels: + build_collection: false + builder_level: season + item_label.remove: Overlay + plex_search: + all: + season_label: Overlay + title: NAME OF SHOW HERE + ``` + +??? "Removing the Overlay label from episodes (click to expand)" + + ```yaml + collections: + Remove Overlay Labels: + build_collection: false + builder_level: episode + item_label.remove: Overlay + plex_search: + all: + episode_label: Overlay + title: NAME OF SHOW HERE + ``` + diff --git a/docs/kometa/guides/scheduling.md b/docs/kometa/guides/scheduling.md index d3af6be33..d39ebff0b 100644 --- a/docs/kometa/guides/scheduling.md +++ b/docs/kometa/guides/scheduling.md @@ -22,7 +22,7 @@ to without any visible impact to the user (other than the Plex libraries and pla This page discusses how to set up this "When should Kometa run" level of schedule, and it leverages both the "modes" of running discussed above. **You can also put schedule information in your config to control things like "process the Movie library only on Tuesdays" or the like. -That level of scheduling is configured independently from the "when Kometa runs" scheduling covered on this page, and is discussed [here](../../config/schedule.md)** +That level of scheduling is configured independently from the "when Kometa runs" scheduling covered on this page, and is discussed [here](../../config/schedule)** IMPORTANT: Every time you see `/path/to/` below, it's a placeholder for the path to that directory on *your* system. You will need to change that to the relevant path on your machine. The same goes for things like `YOUR_USERNAME`. @@ -40,8 +40,8 @@ not required or specifically endorsed beyond "Here's a thing that the author use When running Kometa within docker, the session will resume after a system reboot (assuming Docker is set to start at system startup, which is the default) and Kometa will run in the background at all times. - There's a [Docker Walkthrough](../install/docker.md) with more detailed instructions on setting up Kometa within docker. The simplest command to facilitate a docker run is: - + There's a [Docker Walkthrough](../../install/walkthroughs/docker) with more detailed instructions on setting up Kometa within docker. The simplest command to facilitate a docker run is: + ``` docker run -d \ --restart=unless-stopped \ @@ -62,11 +62,11 @@ not required or specifically endorsed beyond "Here's a thing that the author use This will run Kometa in the background persistently until it is stopped by the user. While the docker container will be persistently running, Kometa will not begin the run until the scheduled time. - Further customizations of the docker run command can be used to specify set times to run Kometa, further information on this and other Run Commands can be found [here](../environmental.md#times) + Further customizations of the docker run command can be used to specify set times to run Kometa, further information on this and other Run Commands can be found [here](../../environmental#times) === "Windows Task Scheduler" - Windows Task Scheduler is advised for those who followed the Windows instructions in the [Local Walkthrough Guides](../install/local.md) and/or do not want to run Kometa within docker. + Windows Task Scheduler is advised for those who followed the Windows instructions in the [Local Walkthrough Guides](../../install/walkthroughs/local) and/or do not want to run Kometa within docker. Windows Task Scheduler allows the user to run commands and services at scheduled times and intervals. @@ -76,8 +76,8 @@ not required or specifically endorsed beyond "Here's a thing that the author use These will be explained further down this page. - These guides assume the user has followed the Windows instructions in the [Local Walkthrough Guides](../install/local.md) which includes setting up the - [virtual environment](../install/local.md#setting-up-a-virtual-environment). Please also ensure to edit any commands to be reflective of the live + These guides assume the user has followed the Windows instructions in the [Local Walkthrough Guides](../../install/walkthroughs/local) which includes setting up the + [virtual environment](../../install/walkthroughs/local#setting-up-a-virtual-environment). Please also ensure to edit any commands to be reflective of the live environment (such as usernames, installation directories). ### Background Run Scheduled Task @@ -87,7 +87,7 @@ not required or specifically endorsed beyond "Here's a thing that the author use This is the recommended approach as it allows the user additional control over how and when Kometa processes. - If you don't specify a time, the script will run at 5AM each day. You can change this with the [time-to-run](../environmental.md#times) runtime flag. + If you don't specify a time, the script will run at 5AM each day. You can change this with the [time-to-run](../../environmental#times) runtime flag. ??? abstract "Background Run Scheduled Task" @@ -272,8 +272,6 @@ not required or specifically endorsed beyond "Here's a thing that the author use ``` - A useful tool to generate these plist files is [https://zerolaunched.herokuapp.com/](https://zerolaunched.herokuapp.com/) - Save this file as `com.YOUR_USERNAME.kometa.plist` in `~/Library/LaunchAgents`. 2. Load and start the agent 🚀 @@ -332,7 +330,7 @@ not required or specifically endorsed beyond "Here's a thing that the author use Change `/path/to/kometa` to reflect where you've installed Kometa. - NOTE: This is assuming you created the `kometa-venv` virtual environment as described in the [Local Walkthrough](../install/local.md) + NOTE: This is assuming you created the `kometa-venv` virtual environment as described in the [Local Walkthrough](../../install/walkthroughs/local) Save and close the file. @@ -371,10 +369,10 @@ not required or specifically endorsed beyond "Here's a thing that the author use This is an example, which does nothing but run the script immediately. If you want to add additional flags you can do so. - NOTE: This is assuming you created the `kometa-venv` virtual environment as described in the [Local Walkthrough](../install/local.md) + NOTE: This is assuming you created the `kometa-venv` virtual environment as described in the [Local Walkthrough](../../install/walkthroughs/local) 2. Open the system crontab for editing: - + ```bash sudo crontab -e ``` diff --git a/docs/kometa/install/walkthroughs/local.md b/docs/kometa/install/walkthroughs/local.md index b5fa8deb5..c0755120a 100644 --- a/docs/kometa/install/walkthroughs/local.md +++ b/docs/kometa/install/walkthroughs/local.md @@ -73,15 +73,15 @@ If this doesn't return a version number, you'll need to get git installed. === ":fontawesome-brands-linux: Linux" - The git install is discussed here: [Download for Linux and Unix](https://git-scm.com/download/linux) + The git install is discussed here: [Download for Linux and Unix](https://git-scm.com/downloads/linux) === ":fontawesome-brands-apple: macOS" - The git install is discussed here: [Git - Downloading Package](https://git-scm.com/download/mac) + The git install is discussed here: [Git - Downloading Package](https://git-scm.com/downloads/mac) === ":fontawesome-brands-windows: Windows" - Download the installer from [here](https://git-scm.com/download/windows) + Download the installer from [here](https://git-scm.com/downloads/win) Run the install; you can probably just accept the defaults and click through except for the step that asks you to choose an editor; you probably want to choose something other than the default there: diff --git a/docs/kometa/install/walkthroughs/truenas.md b/docs/kometa/install/walkthroughs/truenas.md new file mode 100644 index 000000000..3359989f7 --- /dev/null +++ b/docs/kometa/install/walkthroughs/truenas.md @@ -0,0 +1,117 @@ +--- +hide: + - toc +--- +{% + include-markdown "./../../../templates/walkthrough/container.md" + replace='{ + "CONTAINER": "TrueNAS Scale", + "NAS_TYPE": "TrueNAS Scale" + }' +%} + +### Kometa Installation Walkthrough for TrueNAS Scale + +Thanks to i.am.stonecutter on Discord. + +This guide provides step-by-step instructions to install Kometa on TrueNAS Scale (Community Edition, version 25.04.0 or later) using a Docker container. + +#### Prerequisites: + +1. TrueNAS Scale (version 25.04.0 or later) with internet access +2. Administrative access to TrueNAS Scale +3. 'sudo' privileges for terminal access +4. Plex Media Server installed +5. Basic understanding of Docker and YAML + +### Setup + +#### Step 1: Sign into TrueNAS Web Interface + +1. Log in to the TrueNAS Scale web interface. + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-1.png) + +#### Step 2: Create Kometa Dataset + +1. Navigate to Datasets. + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-2a.png) + +2. Create a dataset named kometa using the Apps preset. + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-2b.png) + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-2c.png) + +3. Edit the ACL for the kometa dataset to ensure proper permissions for your user setup. Default settings are usually sufficient, but adjust if issues arise. + +#### Step 3: Set Up Config Directory + +1. Connect to TrueNAS Scale via SSH. +2. Run `sudo mc` to launch Midnight Commander. +3. Navigate to the kometa dataset root. +4. Create a folder named `config`. +5. Copy your `config.yml` file to the config directory using Midnight Commander. + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-3.png) + +#### Step 4: Deploy Kometa Docker Container + +1. In the TrueNAS web interface, go to Apps > Discover Apps. +2. Click the three-dot menu and select **Install via YAML**. +3. In the YAML window, set the container name to `kometa` and use the following docker-compose template, editing the timezone, volume mapping, and UID/GID as needed: + + ```yaml + services: + kometa: + image: kometateam/kometa:latest + environment: + - TZ=America/New York + - KOMETA_CONFIG=/config/config.yml + network + mode: host + restart: unless-stopped + user: "568:568" + volumes: + - /mnt/YOUR-STORAGE-POOL/YOUR-DATASET/kometa/config:/config:rw + ``` + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-4a.png) + +4. Deploy the YAML and verify the `kometa` status shows RUNNING in green. + + ![Prerequisite 1](./../../../assets/images/kometa/install/truenas/truenas-step-4b.png) + +#### Step 5: Run Kometa + +This is an optional step you can perform if you want to fire off a run right this second to verify that things are working. Whether or not you do this, Kometa will wake up tomorrow at 5AM to process the config [assuming you used the docker-compose YAML just above]. + +1. Return to the TrueNAS Scale SSH session. +2. Execute the following command to force an immediate library refresh based on your `config.yml`: + + ``` + sudo docker run --rm -it -v "/mnt/YOUR-STORAGE-POOL/YOUR-DATASET/kometa/config:/config:rw" kometateam/kometa --run + ``` + + Of course, replace `YOUR-STORAGE-POOL/YOUR-DATASET` to suit your specific setup. + +#### Step 6: Verify and Expand + +1. Check your Plex library for updated collections. +2. Explore Kometa’s Defaults for pre-made collections. +3. Refer to the Kometa Wiki for advanced configurations. + +### Troubleshooting + +Container not running: Check Docker logs in TrueNAS for errors. + +Config issues: Verify `config.yml` syntax and permissions. + +Plex not updating: Confirm Plex URL, token, and network settings. + +### Notes + +The `config.yml` is portable; adjust paths if moving. + +Regularly update the Kometa image via Apps > Update. diff --git a/docs/kometa/install/walkthroughs/unraid.md b/docs/kometa/install/walkthroughs/unraid.md index 3ea3c1f37..39161d524 100644 --- a/docs/kometa/install/walkthroughs/unraid.md +++ b/docs/kometa/install/walkthroughs/unraid.md @@ -19,20 +19,20 @@ hide: ### Installing Community Applications in unRAID -Thankfully, getting Kometa working on unRAID is a fairly simple task. unRAID works mostly with docker containers, so the pre-built container available on docker hub works -perfectly with a little configuration. To install a container from docker hub, you will need community applications - a very popular plugin for unRAID servers. +Thankfully, getting Kometa working on unRAID is a fairly simple task. unRAID works mostly with docker containers, so the pre-built container available on docker hub works +perfectly with a little configuration. To install a container from docker hub, you will need community applications - a very popular plugin for unRAID servers. If you don't already have this installed, you can install it [here](https://forums.unraid.net/topic/38582-plug-in-community-applications/) ### Installing Kometa in unRAID -1. Head to the `Apps` tab of unRAID (Community Applications), and search `kometa` in the upper left search box. There will be a couple of results shown, +1. Head to the `Apps` tab of unRAID (Community Applications), and search `kometa` in the upper left search box. There will be a couple of results shown, but you should ignore them ([Why?](../images.md)) and use the official image. ![unraid-options](./../../../assets/images/kometa/install/unraid-options.png) 2. Click the `Install` button on the Template from Sohjiro's Repository Tools. -3. Choose which branch you want to run `latest`, `develop`, or `nightly`. +3. Choose which branch you want to run `latest`, `develop`, or `nightly`. 4. Set the `Console shell command:` to `Bash` @@ -51,7 +51,7 @@ You should see something like this in the unRAID logs for the container: ```shell { .no-copy } Config Error: config not found at //config ``` -We need to have a file called `config.yml` in the correct directory for Kometa to start. +We need to have a file called `config.yml` in the correct directory for Kometa to start. Open up an unRAID terminal session by clicking on the terminal icon `>_` in the top right of unRAID. === ":fontawesome-brands-linux: unRAID" @@ -66,9 +66,11 @@ You can now close the unRAID terminal and start the Kometa container. **_From this point forward, you can Console `>_Console` into the running container as it will stay running_** -Open a Console by left-clicking on the running Kometa Container and selecting `>_Console`. +Open a Console by left-clicking on the running Kometa Container and selecting `>_Console`. This will open up an interactive session within the container to be able to run the commands we want. +![](../../../assets/images/kometa/install/unraid-console.png) + {% include-markdown "./../../../templates/walkthrough/mid.md" include-tags='all|unraid|local-unraid|docker-unraid' diff --git a/docs/kometa/logs.md b/docs/kometa/logs.md index af360f825..ded4028a3 100644 --- a/docs/kometa/logs.md +++ b/docs/kometa/logs.md @@ -10,9 +10,15 @@ hide: The meta.log file can be found within the `logs` folder of your Kometa config folder [right next to `config.yml`]. -`meta.log` is the most recent run of Kometa, `meta.log.1` is the previous run, `meta.log.2` is the run before that, so on and so forth. +`meta.log` is the most recent run of Kometa. -As new log files are created, the old ones get a numeric suffix added: `meta.log.1`. **The most recent is always the one without a number at the end.** +As new log files are created, the old ones get a numeric suffix added: `meta-1.log`. **The most recent is always the one without a number at the end.** + +`meta-1.log` is the previous run, `meta-2.log` is the run before that, so on and so forth. + +???+ info "Old Naming Convention" + + In an older release of Kometa, prevous run logs followed the naming pattern of `meta.log.1`, `meta.log.2` etc. These files can safely be renamed if you no longer require them. ### Providing Log Files on Discord diff --git a/docs/old_md/year_variables.md b/docs/old_md/year_variables.md index 9c3d98bee..806bfca11 100644 --- a/docs/old_md/year_variables.md +++ b/docs/old_md/year_variables.md @@ -1,8 +1,8 @@ -| `collection_order` | **Description:** Changes the Collection Order for all collections in a Defaults File.
**Default:** `custom`
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom`Order Collection Via the Builder Order
[Any `plex_search` Sort Option](../../files/builders/plex.md#sort-options)Order Collection by any `plex_search` Sort Option
| -| `collection_order_<>`**1** | **Description:** Changes the Collection Order of the [key's](#collection_section) collection.
**Default:** `collection_order`
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom`Order Collection Via the Builder Order
[Any `plex_search` Sort Option](../../files/builders/plex.md#sort-options)Order Collection by any `plex_search` Sort Option
| +| `collection_order` | **Description:** Changes the Collection Order for all collections in a Defaults File.
**Default:** `custom`
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom`Order Collection Via the Builder Order
[Any `plex_search` Sort Option](../../files/builders/plex/sort-options.md)Order Collection by any `plex_search` Sort Option
| +| `collection_order_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Collection Order of the [key's](#collection_section) collection.
**Default:** `collection_order`
**Values:**
`release`Order Collection by Release Dates
`alpha`Order Collection Alphabetically
`custom`Order Collection Via the Builder Order
[Any `plex_search` Sort Option](../../files/builders/plex/sort-options.md)Order Collection by any `plex_search` Sort Option
| | `data` | **Description:** Replaces the `data` dynamic collection value.
AttributeDescription & Values
startingControls the starting year for collections
Default: latest-5
Values: Number greater than 0
endingControls the ending year for collections
Default: latest
Values: Number greater than 1
incrementControls the increment (i.e. every 5th year)
Default: 1
Values: Number greater than 0
  • starting and ending can also have the value latest
  • You can also use a value relative to the latest by doing latest-5
| -| `exclude` | **Description:** Exclude these Years from creating a Dynamic Collection.
**Values:** List of Years | +| `exclude` | **Description:** Exclude these Years from creating a Dynamic Collection.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Years | | `sync_mode` | **Description:** Changes the Sync Mode for all collections in a Defaults File.
**Default:** `sync`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| -| `sync_mode_<>`**1** | **Description:** Changes the Sync Mode of the [key's](#collection_section) collection.
**Default:** `sync_mode`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| +| `sync_mode_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Sync Mode of the [key's](#collection_section) collection.
**Default:** `sync_mode`
**Values:**
`sync`Add and Remove Items based on Builders
`append`Only Add Items based on Builders
| | `use_year_collections` | **Description:** Turn the individual year collections off.
**Values:** `false` to turn of the collections | | `year_collection_section` | **Description:** Change the collection section for year collections only. (Use quotes to not lose leading zeros `"05"`)
**Values:** Any number | diff --git a/docs/overrides/main.html b/docs/overrides/main.html index f0a3588fc..e3b38201a 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -45,4 +45,6 @@ document.write(announceText); + {% endblock %} + diff --git a/docs/overrides/partials/footer.html b/docs/overrides/partials/footer.html index 15bc346e6..2b7d89003 100644 --- a/docs/overrides/partials/footer.html +++ b/docs/overrides/partials/footer.html @@ -22,4 +22,6 @@ {% include "partials/social.html" %}
- \ No newline at end of file + + +{% include "partials/tooltips.html" %} \ No newline at end of file diff --git a/docs/overrides/partials/tooltips.html b/docs/overrides/partials/tooltips.html new file mode 100644 index 000000000..4af62b115 --- /dev/null +++ b/docs/overrides/partials/tooltips.html @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/templates/defaults/base/collection/header.md b/docs/templates/defaults/base/collection/header.md index 69309e01b..efad84433 100644 --- a/docs/templates/defaults/base/collection/header.md +++ b/docs/templates/defaults/base/collection/header.md @@ -7,13 +7,12 @@ The `CODE_NAME` Default Collection File is used to DESCRIPTION. ![header image](../../../../assets/images/defaults/posters/CODE_NAME.png) - ## Requirements & Recommendations Supported Library Types: LIBRARY_TYPE - + ## Collections Section SECTION_NUMBER | Collection | Key | Description | diff --git a/docs/templates/defaults/base/collection/shared.md b/docs/templates/defaults/base/collection/shared.md index f270fedd7..77fb3f650 100644 --- a/docs/templates/defaults/base/collection/shared.md +++ b/docs/templates/defaults/base/collection/shared.md @@ -6,68 +6,65 @@ |:----------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `collection_mode` | **Description:** Controls the collection mode of all collections in a Defaults File.
**Values:**
`default`Library default
`hide`Hide Collection
`hide_items`Hide Items in this Collection
`show_items`Show this Collection and its Items
| | `collection_section` | **Description:** Changes the sort order of the collection sections against other default collection sections. (Use quotes to not lose leading zeros `"05"`)
**Values:** Any number | - | `delete_collections_named` | **Description:** Used to delete any collections in your plex named one of the given collections.
**Values:** List of Collection Names to delete | - | `file_background_<>`**1** | **Description:** Sets the background filepath of the key's collection.
**Values:** Filepath directly to the Image | + | `delete_collections_named` | **Description:** Used to delete any collections in your plex named one of the given collections.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Collection Names to delete | + | `file_background_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the background filepath of the key's collection.
**Values:** Filepath directly to the Image | | `file_background` | **Description:** Sets the background filepath for all collections.
**Values:** Filepath directly to the Image | - | `file_poster_<>`**1** | **Description:** Sets the poster filepath of the key's collection.
**Values:** Filepath directly to the Image | + | `file_poster_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the poster filepath of the key's collection.
**Values:** Filepath directly to the Image | | `file_poster` | **Description:** Sets the poster filepath for all collections.
**Values:** Filepath directly to the Image | - | `ignore_ids` | **Description:** Set a list or comma-separated string of TMDb/TVDb IDs to ignore in all collections.
**Values:** List or comma-separated string of TMDb/TVDb IDs | - | `ignore_imdb_ids` | **Description:** Set a list or comma-separated string of IMDb IDs to ignore in all collections.
**Values:** List or comma-separated string of IMDb IDs | - | `item_radarr_tag_<>`**1** | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr of the key's collection.
**Default:** `item_radarr_tag`
**Values:** List or comma-separated string of tags | - | `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr for all collections in a Defaults File.
**Values:** List or comma-separated string of tags | - | `item_sonarr_tag_<>`**1** | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr of the key's collection.
**Default:** `item_sonarr_tag`
**Values:** List or comma-separated string of tags | - | `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr for all collections in a Defaults File.
**Values:** List or comma-separated string of tags | + | `ignore_ids` | **Description:** Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs to ignore in all collections.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs | + | `ignore_imdb_ids` | **Description:** Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs to ignore in all collections.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs | + | `item_radarr_tag_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr of the key's collection.
**Default:** `item_radarr_tag`
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr for all collections in a Defaults File.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `item_sonarr_tag_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr of the key's collection.
**Default:** `item_sonarr_tag`
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr for all collections in a Defaults File.
**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | | `language` | **Description:** Set the language of Collection Names and Summaries
**Default:** `default`
**Values:** `default` (English), `fr` (French), or `de` (German) | - | `minimum_items_<>`**1** | **Description:** Set the number of minimum items for a collection to be created for the key's collection.
**Values:** Any number greater than 0 | + | `minimum_items_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Set the number of minimum items for a collection to be created for the key's collection.
**Values:** Any number greater than 0 | | `minimum_items` | **Description:** Set the number of minimum items for a collection to be created for all collections.
**Values:** Any number greater than 0 | - | `name_<>`**1** | **Description:** Changes the name of the key's collection.
**Values:** New Collection Name | + | `name_<>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the name of the key's collection.
**Values:** New Collection Name | | `name_mapping` | **Description:** Changes the `name_mapping` of all collections.
**Default:** `<>`<br>**Values:** Any String with `<<key_name>>` in it. | - | `order_<<key>>`<sup>**1**</sup> | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | + | `order_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | | `radarr_add_missing` | **Description:** Override Radarr `add_missing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `radarr_add_missing_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `add_missing` attribute of the key's collection.<br>**Default:** `radarr_add_missing`<br>**Values:** `true` or `false` | + | `radarr_add_missing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `add_missing` attribute of the key's collection.<br>**Default:** `radarr_add_missing`<br>**Values:** `true` or `false` | | `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute for all collections in a Defaults File.<br>**Values:** Folder Path | - | `radarr_folder_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `root_folder_path` attribute of the key's collection.<br>**Default:** `radarr_folder`<br>**Values:** Folder Path | + | `radarr_folder_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `root_folder_path` attribute of the key's collection.<br>**Default:** `radarr_folder`<br>**Values:** Folder Path | | `radarr_monitor_existing` | **Description:** Override Radarr `monitor_existing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `radarr_monitor_existing_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `monitor_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | + | `radarr_monitor_existing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `monitor_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | | `radarr_search` | **Description:** Override Radarr `search` attribute or all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `radarr_search_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `search` attribute of the key's collection.<br>**Default:** `radarr_search`<br>**Values:** `true` or `false` | - | `radarr_tag` | **Description:** Override Radarr `tag` attribute for all collections in a Defaults File.<br>**Values:** List or comma-separated string of tags | - | `radarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `tag` attribute of the key's collection.<br>**Default:** `radarr_tag`<br>**Values:** List or comma-separated string of tags | + | `radarr_search_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `search` attribute of the key's collection.<br>**Default:** `radarr_search`<br>**Values:** `true` or `false` | + | `radarr_tag` | **Description:** Override Radarr `tag` attribute for all collections in a Defaults File.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `radarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `tag` attribute of the key's collection.<br>**Default:** `radarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | | `radarr_upgrade_existing` | **Description:** Override Radarr `upgrade_existing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `radarr_upgrade_existing_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `upgrade_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | + | `radarr_upgrade_existing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `upgrade_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | | `schedule_<<key>>` | **Description:** Set the schedule for a specific key's collection.<br>**Values:** Any [Scheduling Option](../../config/schedule.md#important) | | `schedule` | **Description:** Set the schedule for all of the collections in a Defaults File.<br>**Values:** Any [Scheduling Option](../../config/schedule.md#important) | - | `sonarr_add_missing_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `add_missing` attribute of the key's collection.<br>**Default:** `sonarr_add_missing`<br>**Values:** `true` or `false` | + | `sonarr_add_missing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `add_missing` attribute of the key's collection.<br>**Default:** `sonarr_add_missing`<br>**Values:** `true` or `false` | | `sonarr_add_missing` | **Description:** Override Sonarr `add_missing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `sonarr_folder_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `root_folder_path` attribute of the key's collection.<br>**Default:** `sonarr_folder`<br>**Values:** Folder Path | + | `sonarr_folder_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `root_folder_path` attribute of the key's collection.<br>**Default:** `sonarr_folder`<br>**Values:** Folder Path | | `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute for all collections in a Defaults File.<br>**Values:** Folder Path | - | `sonarr_monitor_existing_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `monitor_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | + | `sonarr_monitor_existing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `monitor_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | | `sonarr_monitor_existing` | **Description:** Override Sonarr `monitor_existing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `sonarr_search_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `search` attribute of the key's collection.<br>**Default:** `sonarr_search`<br>**Values:** `true` or `false` | + | `sonarr_search_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `search` attribute of the key's collection.<br>**Default:** `sonarr_search`<br>**Values:** `true` or `false` | | `sonarr_search` | **Description:** Override Sonarr `search` attribute or all collections in a Defaults File.<br>**Values:** `true` or `false` | - | `sonarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `tag` attribute of the key's collection.<br>**Default:** `sonarr_tag`<br>**Values:** List or comma-separated string of tags | - | `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all collections in a Defaults File.<br>**Values:** List or comma-separated string of tags | - | `sonarr_upgrade_existing_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `upgrade_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | + | `sonarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `tag` attribute of the key's collection.<br>**Default:** `sonarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all collections in a Defaults File.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | + | `sonarr_upgrade_existing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `upgrade_existing` attribute of the key's collection.<br>**Values:** `true` or `false` | | `sonarr_upgrade_existing` | **Description:** Override Sonarr `upgrade_existing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | | `sort_prefix` | **Description:** Changes the prefix of the sort title.<br>**Default:** `!`<br>**Values:** Any String | | `sort_title` | **Description:** Changes the sort title of all collections.<br>**Default:** `<<sort_prefix>><<collection_section>><<pre>><<order_<<key>>>><<title>>`<br>**Values:** Any String | - | `summary_<<key>>`<sup>**1**</sup> | **Description:** Changes the summary of the key's collection.<br>**Values:** New Collection Summary | - | `url_background_<<key>>`<sup>**1**</sup> | **Description:** Sets the background url of the key's collection.<br>**Values:** URL directly to the Image | + | `summary_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the summary of the key's collection.<br>**Values:** New Collection Summary | + | `url_background_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the background url of the key's collection.<br>**Values:** URL directly to the Image | | `url_background` | **Description:** Sets the background url for all collections.<br>**Values:** URL directly to the Image | - | `url_poster_<<key>>`<sup>**1**</sup> | **Description:** Changes the poster url of the key's collection.<br>**Values:** URL directly to the Image | + | `url_poster_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the poster url of the key's collection.<br>**Values:** URL directly to the Image | | `url_poster` | **Description:** Changes the poster url for all collections.<br>**Values:** URL directly to the Image | - | `use_<<key>>`<sup>**1**</sup> | **Description:** Turns off individual Collections in a Defaults File.<br>**Values:** `false` to turn off the collection | + | `use_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Turns off individual Collections in a Defaults File.<br>**Values:** `false` to turn off the collection | | `use_all` | **Description:** Turns off all Collections in a Defaults File.<br>**Values:** `false` to turn off the collection | - | `visible_home_<<key>>`<sup>**1**</sup> | **Description:** Controls visible on Home Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_home`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | + | `visible_home_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls visible on Home Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_home`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | | `visible_home` | **Description:** Controls visible on Home Tab for all collections in a Defaults File. (Only works with Plex Pass)<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | - | `visible_library_<<key>>`<sup>**1**</sup> | **Description:** Controls visible on Library Recommended Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_library`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | + | `visible_library_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls visible on Library Recommended Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_library`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | | `visible_library` | **Description:** Controls visible on Library Recommended Tab for all collections in a Defaults File. (Only works with Plex Pass)<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | - | `visible_shared_<<key>>`<sup>**1**</sup> | **Description:** Controls visible on Shared Users' Home Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_shared`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | + | `visible_shared_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls visible on Shared Users' Home Tab of the key's collection. (Only works with Plex Pass)<br>**Default:** `visible_shared`<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | | `visible_shared` | **Description:** Controls visible on Shared Users' Home Tab for all collections in a Defaults File. (Only works with Plex Pass)<br>**Values:**<table class="clearTable"><tr><td>`true`</td><td>Visible</td></tr><tr><td>`false`</td><td>Not Visible</td></tr><tr><td>[Any `schedule` Option](../../config/schedule.md)</td><td>Visible When Scheduled</td></tr></table> | - 1. Each default collection has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling. - <!--separator-variables--> - === "Shared Separator Variables" | Variable | Description & Values | diff --git a/docs/templates/defaults/base/overlays/shared.md b/docs/templates/defaults/base/overlays/shared.md index 1db8d5dca..3bea22e79 100644 --- a/docs/templates/defaults/base/overlays/shared.md +++ b/docs/templates/defaults/base/overlays/shared.md @@ -11,25 +11,25 @@ libraries: Movies: overlay_files: - - default: resolution + - default: CODE_NAME template_variables: horizontal_align: left horizontal_offset: 247 vertical_align: bottom vertical_offset: 40% ``` - That would place the resolution overlay 247 pixels in from the left edge of the poster, and 40% of the way up from the bottom. + That would place the CODE_NAME overlay 247 pixels in from the left edge of the poster, and 40% of the way up from the bottom. ```yaml libraries: Movies: overlay_files: - - default: resolution + - default: CODE_NAME template_variables: back_width: 198 back_height: 47 ``` - That would set the resolution overlay background to 198 pixels wide by 47 pixels high. + That would set the CODE_NAME overlay background to 198 pixels wide by 47 pixels high. Color values should be wrapped in quotes in the YAML, as the `#` denotes a comment in YAML and if left unquoted will prevent the value from being seen by Kometa. @@ -47,21 +47,20 @@ | `back_padding` | **Description:** Controls the Backdrop Padding for the Text Overlay.<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | | `back_radius` | **Description:** Controls the Backdrop Radius for the Text Overlay.<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | | `back_width` | **Description:** Controls the Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | - | `file_<<key>>`<sup>**1**</sup> | **Description:** Controls the image associated with this key's Overlay to a local file.<br>**Values:** Filepath to Overlay Image | + | `file_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a local file.<br>**Values:** Filepath to Overlay Image | | `file` | **Description:** Controls the images associated with all the Overlays to a local file.<br>**Values:** Filepath to Overlay Image | - | `git_<<key>>`<sup>**1**</sup> | **Description:** Controls the image associated with this key's Overlay to the git repo.<br>**Values:** Git Path to Overlay Image | + | `git_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to the git repo.<br>**Values:** Git Path to Overlay Image | | `git` | **Description:** Controls the images associated with all the Overlays to the git repo.<br>**Values:** Git Path to Overlay Image | | `horizontal_align` | **Description:** Controls the Horizontal Alignment of the overlay.<br>**Values:** `left`, `center`, or `right` | | `horizontal_offset` | **Description:** Controls the Horizontal Offset of this overlay. Can be a %.<br>**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | - | `repo_<<key>>`<sup>**1**</sup> | **Description:** Controls the image associated with this key's Overlay to a custom repo.<br>**Values:** Repo Path to Overlay Image | + | `repo_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a custom repo.<br>**Values:** Repo Path to Overlay Image | | `repo` | **Description:** Controls the images associated with all the Overlays to a custom repo.<br>**Values:** Repo Path to Overlay Image | - | `url_<<key>>`<sup>**1**</sup> | **Description:** Controls the image associated with this key's Overlay to a url.<br>**Values:** URL to Overlay Image | + | `url_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the image associated with this key's Overlay to a url.<br>**Values:** URL to Overlay Image | | `url` | **Description:** Controls the images associated with all the Overlays to a url.<br>**Values:** URL to Overlay Image | - | `use_<<key>>`<sup>**1**</sup> | **Description:** Turns off individual Overlays in a Defaults File.<br>**Values:** `false` to turn off the overlay | + | `use_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Turns off individual Overlays in a Defaults File.<br>**Values:** `false` to turn off the overlay | | `vertical_align` | **Description:** Controls the Vertical Alignment of the overlay.<br>**Values:** `top`, `center`, or `bottom` | | `vertical_offset` | **Description:** Controls the Vertical Offset of this overlay. Can be a %.<br>**Values:** Number 0 or greater or 0%-100% [pixels assuming a 1000x1500 image] | - 1. Each default overlay has a `key` that when calling to effect a specific collection you must replace `<<key>>` with when calling. <!--text-variables--> === "Overlay Text Template Variables" @@ -77,16 +76,16 @@ | Variable | Description & Values | |:-----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `font_<<key>>` | **Description:** Choose the font for this key's Overlay.<br>**Default:** `fonts/Inter-Medium.ttf`<br>**Values:** Path to font file | - | `font_color_<<key>>` | **Description:** Choose the font color for this key's Overlay.<br>**Default:** `#FFFFFF`<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | + | `font_color_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Choose the font color for this key's Overlay.<br>**Default:** `#FFFFFF`<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | | `font_color` | **Description:** Choose the font color for the Overlay.<br>**Default:** `#FFFFFF`<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | - | `font_size_<<key>>` | **Description:** Choose the font size for this key's Overlay.<br>**Default:** `55`<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | + | `font_size_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Choose the font size for this key's Overlay.<br>**Default:** `55`<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | | `font_size` | **Description:** Choose the font size for the Overlay.<br>**Default:** `55`<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | - | `font_style_<<key>>` | **Description:** Font style for this key's Variable Fonts.<br>**Values:** Variable Font Style | + | `font_style_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Font style for this key's Variable Fonts.<br>**Values:** Variable Font Style | | `font_style` | **Description:** Font style for Variable Fonts.<br>**Values:** Variable Font Style | | `font` | **Description:** Choose the font for the Overlay.<br>**Default:** `fonts/Inter-Medium.ttf`<br>**Values:** Path to font file | - | `stroke_color_<<key>>` | **Description:** Font Stroke Color for this key's Overlay.<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | + | `stroke_color_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Font Stroke Color for this key's Overlay.<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | | `stroke_color` | **Description:** Font Stroke Color for the Overlay.<br>**Values:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA` | - | `stroke_width_<<key>>` | **Description:** Font Stroke Width for this key's Overlay.<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | + | `stroke_width_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Font Stroke Width for this key's Overlay.<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | | `stroke_width` | **Description:** Font Stroke Width for the Overlay.<br>**Values:** Any number greater than 0 [pixels assuming a 1000x1500 image] | diff --git a/docs/templates/snippets/collection_list.md b/docs/templates/snippets/collection_list.md index c2c52b2a9..24864bf2f 100644 --- a/docs/templates/snippets/collection_list.md +++ b/docs/templates/snippets/collection_list.md @@ -28,8 +28,6 @@ These collections are applied by calling the below paths into the `collection_fi === "Charts" - <div class="annotate" markdown> - | Default | Path | Example Collections | Allowed Media | |:----------------------------------------------|:------------------|:-------------------------------------------|:--------------------| | [Chart Separator](../../chart/separator) | `separator_chart` | Chart Collections | `Movies`<br>`Shows` | @@ -38,16 +36,11 @@ These collections are applied by calling the below paths into the `collection_fi | [IMDb Charts](../../chart/imdb) | `imdb` | IMDb Popular, IMDb Top 250 | `Movies`<br>`Shows` | | [Letterboxd Charts](../../chart/letterboxd) | `letterboxd` | Letterboxd Top 250, Top 250 Most Fans | `Movies` | | [MyAnimeList Charts](../../chart/myanimelist) | `myanimelist` | MyAnimeList Popular, MyAnimeList Top Rated | `Movies`<br>`Shows` | - | [Tautulli Charts](../../chart/tautulli)(1) | `tautulli` | Plex Popular, Plex Watched | `Movies`<br>`Shows` | + | [Tautulli Charts](../../chart/tautulli) :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-collection-list-1" } | `tautulli` | Plex Popular, Plex Watched | `Movies`<br>`Shows` | | [TMDb Charts](../../chart/tmdb) | `tmdb` | TMDb Popular, TMDb Airing Today | `Movies`<br>`Shows` | - | [Trakt Charts](../../chart/trakt)(2) | `trakt` | Trakt Popular, Trakt Trending | `Movies`<br>`Shows` | + | [Trakt Charts](../../chart/trakt) :material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-collection-list-2" } | `trakt` | Trakt Popular, Trakt Trending | `Movies`<br>`Shows` | | [Other Charts](../../chart/other) | `other_chart` | AniDB Popular, Common Sense Selection | `Movies`<br>`Shows` | - </div> - - 1. Requires [Tautulli Authentication](../../../config/tautulli)<br> - 2. Requires [Trakt Authentication](../../../config/trakt) - === "Content" | Default | Path | Example Collections | Allowed Media | @@ -101,7 +94,7 @@ These collections are applied by calling the below paths into the `collection_fi | Default | Path | Example Collections | Allowed Media | |:----------------------------------|:------------|:-----------------------------------------|:--------------------| | [Networks](../../show/network) | `network` | Disney Channel, Lifetime | `Shows` | - | [Streaming](../../both/streaming) | `streaming` | Disney+ Movies, Max Shows | `Movies`<br>`Shows` | + | [Streaming](../../both/streaming) | `streaming` | Disney+ Movies, HBO Max Shows | `Movies`<br>`Shows` | | [Studios](../../both/studio) | `studio` | DreamWorks Studios, Walt Disney Pictures | `Movies`<br>`Shows` | === "Time" diff --git a/docs/templates/snippets/overlay_list.md b/docs/templates/snippets/overlay_list.md index 3416ff0fe..a63a15acd 100644 --- a/docs/templates/snippets/overlay_list.md +++ b/docs/templates/snippets/overlay_list.md @@ -15,19 +15,13 @@ These overlays are applied by calling the below paths into the `overlay_files` [ === "Content" - <div class="annotate" markdown> - | Default | Path | Example Overlays | Allowed Media | |:--------------------------------------------|:---------------|:-------------------------------------------------|:----------------------------------| | [Episode Info](../../overlays/episode_info) | `episode_info` | "S01E01", "S02E09" | `Episodes` | - | [Mediastinger](../../overlays/mediastinger) | `mediastinger` | Mediastinger Logo for After/During Credit Scenes | `Movies`<br>`Shows` | - | [Ratings](../../overlays/ratings)(1) | `ratings` | IMDb Audience Rating, Metacritic Critic Rating | `Movies`<br>`Shows`<br>`Episodes` | + | [Mediastinger](../../overlays/mediastinger) | `mediastinger` | Mediastinger Logo for After/During Credit Scenes | `Movies` | + | [Ratings](../../overlays/ratings) :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-1" } | `ratings` | IMDb Audience Rating, Metacritic Critic Rating | `Movies`<br>`Shows`<br>`Episodes` | | [Status](../../overlays/status) | `status` | Airing, Returning, Canceled, Ended | `Shows` | - </div> - - 1. Requires Template Variables to function - === "Content Ratings" | Default | Path | Example Overlays | Allowed Media | @@ -40,31 +34,18 @@ These overlays are applied by calling the below paths into the `overlay_files` [ | [NZ Content Ratings](../../overlays/content_rating_nz) | `content_rating_nz` | G, PG, M, R13, RP13, R15, R16, RP16, R18, RP18, R, NR | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | | [Common Sense Age Rating](../../overlays/commonsense) | `commonsense` | 1+, 2+, 3+, 4+, ..., 17+, 18+, NR | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | - - === "Media" - <div class="annotate" markdown> - | Default | Path | Example Overlays | Allowed Media | |:---------------------------------------------------------------|:-----------------|:--------------------------------------------------------------------------|:-----------------------------------------------------| - | [Aspect Ratio](../../overlays/aspect)(1) | `aspect` | "1.33","1.78" | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | - | [Audio Codec](../../overlays/audio_codec)(2) | `audio_codec` | Dolby Atmos logo, DTS logo | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | + | [Aspect Ratio](../../overlays/aspect) | `aspect` | "1.33","1.78" | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | + | [Audio Codec](../../overlays/audio_codec):material-numeric-2-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-2" } | `audio_codec` | Dolby Atmos logo, DTS logo | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | | [Audio/Subtitle Language Count](../../overlays/language_count) | `language_count` | Dual-Audio, Multi-Audio, Dual-Subtitle, Multi-Subtitle | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | | [Audio/Subtitle Language Flags](../../overlays/languages) | `languages` | Flags Based on the Audio/Subtitles a file has | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | - | [Resolution/Editions](../../overlays/resolution)(3) | `resolution` | 4K Dolby Vision logo, 720P logo, "Director's Cut", "Criterion Collection" | `Movies`<br>`Shows`<br>`Episodes` | + | [Resolution/Editions](../../overlays/resolution) :material-numeric-3-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-3" } | `resolution` | 4K Dolby Vision logo, 720P logo, "Director's Cut", "Criterion Collection" | `Movies`<br>`Shows`<br>`Episodes` | | [Runtimes](../../overlays/runtimes) | `runtimes` | "Runtime: 1h 30m" | `Movies`<br>`Shows`<br>`Episodes` | | [Versions](../../overlays/versions) | `versions` | Multiple Versions logo | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | - | [Video Format](../../overlays/video_format)(4) | `video_format` | "REMUX", "HDTV" | `Movies`<br>`Shows`(5)<br>`Seasons`(6)<br>`Episodes` | - - </div> - - 1. Designed to use the [TRaSH Guides](../../https://trash-guides.info/) filename naming scheme - 2. Designed to use the [TRaSH Guides](../../https://trash-guides.info/) filename naming scheme - 3. Editions overlay is designed to use the Editions field within Plex [which requires Plex Pass to use] or the [TRaSH Guides](../../https://trash-guides.info/) filename naming scheme - 4. Designed to use the [TRaSH Guides](../../https://trash-guides.info/) filename naming scheme - 5. While these overlays can technically be applied at this level, they were not designed for it. For example, a show's season cannot have a resolution since it is not a video file, and an episode cannot have a Common Sense rating since only Movies and Shows are rated by Common Sense. - 6. While these overlays can technically be applied at this level, they were not designed for it. For example, a show's season cannot have a resolution since it is not a video file, and an episode cannot have a Common Sense rating since only Movies and Shows are rated by Common Sense. + | [Video Format](../../overlays/video_format) :material-numeric-4-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-4" } | `video_format` | "REMUX", "HDTV" | `Movies`<br>`Shows` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-5" }<br>`Seasons` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-5" }<br>`Episodes` | === "Production" @@ -75,14 +56,7 @@ These overlays are applied by calling the below paths into the `overlay_files` [ | [Studio](../../overlays/studio) | `studio` | "Warner Bros. Pictures", "Amblin Entertainment" | `Movies`<br>`Shows`<br>`Seasons`<br>`Episodes` | === "Utility" - - <div class="annotate" markdown> | Default | Path | Example Overlays | Allowed Media | |:------------------------------------------|:--------------|:-------------------|:-----------------------------------------------------| - | [Direct Play](../../overlays/direct_play) | `direct_play` | "Direct Play Only" | `Movies`<br>`Shows`(1)<br>`Seasons`(2)<br>`Episodes` | - - </div> - - 1. While these overlays can technically be applied at this level, they were not designed for it. For example, a show's season cannot have a resolution since it is not a video file, and an episode cannot have a Common Sense rating since only Movies and Shows are rated by Common Sense. - 2. While these overlays can technically be applied at this level, they were not designed for it. For example, a show's season cannot have a resolution since it is not a video file, and an episode cannot have a Common Sense rating since only Movies + | [Direct Play](../../overlays/direct_play) | `direct_play` | "Direct Play Only" | `Movies`<br>`Shows` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-5" }<br>`Seasons` :material-numeric-5-circle:{ data-tooltip data-tooltip-id="tippy-overlay-list-5" }<br>`Episodes` | diff --git a/docs/templates/snippets/plex_builder_attributes.md b/docs/templates/snippets/plex_builder_attributes.md index 89befaacc..4b1b34231 100644 --- a/docs/templates/snippets/plex_builder_attributes.md +++ b/docs/templates/snippets/plex_builder_attributes.md @@ -1,10 +1,10 @@ === "Builder Attributes" - The majority of Smart and Manual Builders utilize the same Builder Attributes. Any deviation from this will be highlighted against the specific Builder. +The majority of Smart and Manual Builders utilize the same Builder Attributes. Any deviation from this will be highlighted against the specific Builder. - | Attribute | Description & Values | - |:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| - | `limit` | **Description:** The max number of item for the filter.<br>**Default:** `all`<br>**Values:** `all` or a number greater than 0 | - | `sort_by` | **Description:** This will control how the filter is sorted in your library. You can do a multi-level sort using a list.<br>**Default:** `random`<br>**Values:** Any sort options for your filter type in the [Sorts Options Table](#sort-options) | - | `validate` | **Description:** Determines if a collection will fail on a validation error<br>**Default:** `true`<br>**Values**: `true` or `false` | +| Attribute | Description & Values | +|:-----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | **Description:** The max number of item for the filter.<br>**Default:** `all`<br>**Values:** `all` or a number greater than 0 | +| `sort_by` | **Description:** This will control how the filter is sorted in your library. You can do a multi-level sort using a list.<br>**Default:** `random`<br>**Values:** Any sort options for your filter type in the [Sorts Options Table](#sort-options) | +| `validate` | **Description:** Determines if a collection will fail on a validation error<br>**Default:** `true`<br>**Values**: `true` or `false` | diff --git a/docs/templates/snippets/plex_search_options.md b/docs/templates/snippets/plex_search_options.md index 37b28effc..e79b5d758 100644 --- a/docs/templates/snippets/plex_search_options.md +++ b/docs/templates/snippets/plex_search_options.md @@ -36,7 +36,8 @@ | `artist_unmatched` | Is Artist's Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `album_unmatched` | Is Album's Unmatched | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `track_trash` | Is Track Trashed | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - + | `dovi` | Has Dolby Vision | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + === "Date Filters" Date filters can be used with either no modifier or with `.not`, `.before`, or `.after`. @@ -90,10 +91,10 @@ | `audience_rating` | Uses the audience rating attribute to match<br>**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `user_rating` | Uses the user rating attribute to match<br>**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `episode_user_rating` | Uses the user rating attribute of the show's episodes to match<br>**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `year`<sup>**1**</sup> | Uses the year attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `episode_year`<sup>**1**</sup> | Uses the Episode's year attribute to match<br> **Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `album_year`<sup>**1**</sup> | Uses the Album's year attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - | `album_decade`<sup>**1**</sup> | Uses the Album's decade attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the Episode's year attribute to match<br> **Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `album_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the Album's year attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | + | `album_decade` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the Album's decade attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `album_plays` | Uses the Album's plays attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `track_plays` | Uses the Track's plays attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `track_skips` | Uses the Track's skips attribute to match<br>**Minimum:** `0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | @@ -102,9 +103,6 @@ | `album_critic_rating` | Uses the Album's critic rating attribute to match<br>**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `track_user_rating` | Uses the Track's user rating attribute to match<br>**Range:** `0.0` - `10.0` | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - <sup>**1**</sup> You can use `current_year` to have Kometa use the current years value. This can be combined with a - `-#` at the end to subtract that number of years. i.e. `current_year-2` - ???+ tip "Number Filter Modifiers" | Number Modifier | Description | Plex Web UI Display | @@ -152,7 +150,7 @@ Tag filters can be used with either no modifier or with `.not` except for `decade` and `resolution` which can only be used with no modifier. - Tag filter can take multiple values as a **list or a comma-separated string**. + Tag filter can take multiple values as a **list :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or a comma-separated string**. #### Tag Filter Attributes @@ -166,7 +164,7 @@ | `episode_collection` | Uses the collection tags to match for episode collections | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `content_rating` | Uses the content rating tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `country` | Uses the country tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `decade`<sup>**1**</sup> | Uses the year tag to match the decade | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | + | `decade` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year tag to match the decade | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | | `director` | Uses the director tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | | `genre` | Uses the genre tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `label` | Uses the label tags to match for top level collections | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | @@ -177,8 +175,8 @@ | `resolution` | Uses the resolution tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `subtitle_language` | Uses the subtitle language tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `writer` | Uses the writer tags to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | - | `year`<sup>**1**</sup> | Uses the year tag to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | - | `episode_year`<sup>**1**</sup> | Uses the year tag to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" } | Uses the year tag to match | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | + | `episode_year` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }| Uses the year tag to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-xmark:{ .red } | | `artist_genre` | Uses the Artist's Genre attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `artist_collection` | Uses the Artist's Collection attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `artist_country` | Uses the Artist's Country attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | @@ -197,7 +195,7 @@ | `track_source` | Uses the Track's Source attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | | `track_label` | Uses the Track's Label attribute to match | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-xmark:{ .red } | :fontawesome-solid-circle-check:{ .green } | - <sup>**1**</sup> You can use `current_year` to have Kometa use the current years value. This can be combined with a + :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-plex-search-1" }You can use `current_year` to have Kometa use the current years value. This can be combined with a `-#` at the end to subtract that number of years. i.e. `current_year-2` ???+ tip "Tag Filter Modifiers" diff --git a/docs/templates/snippets/standard_style.md b/docs/templates/snippets/standard_style.md index 3ae61bdfb..65b9692c0 100644 --- a/docs/templates/snippets/standard_style.md +++ b/docs/templates/snippets/standard_style.md @@ -2,4 +2,4 @@ Below is a screenshot of the alternative Standard (`standard`) style which can be set via the `style` Template Variable. -![](../../assets/images/defaults/styles/CODE_NAME_standard.png) \ No newline at end of file +![](../../../assets/images/defaults/overlays/CODE_NAME_standard.png) diff --git a/docs/templates/snippets/tooltips/arr_radarr.md b/docs/templates/snippets/tooltips/arr_radarr.md deleted file mode 100644 index 8e89d194c..000000000 --- a/docs/templates/snippets/tooltips/arr_radarr.md +++ /dev/null @@ -1,39 +0,0 @@ -1. Lists in YAML are represented by using the hyphen (-) and space. They are ordered and can be embedded inside a map using indentation. - - ```{ .yaml .no-copy } - asset_directory: - - config/movie assets - - config/tv assets - ``` - - The first item in the list is `config/movie assets` and the second is `config/tv assets`. - -2. Lists in YAML are represented by using the hyphen (-) and space. They are ordered and can be embedded inside a map using indentation. - - ```{ .yaml .no-copy } - asset_directory: - - config/movie assets - - config/tv assets - ``` - - The first item in the list is `config/movie assets` and the second is `config/tv assets`. - -3. Lists in YAML are represented by using the hyphen (-) and space. They are ordered and can be embedded inside a map using indentation. - - ```{ .yaml .no-copy } - asset_directory: - - config/movie assets - - config/tv assets - ``` - - The first item in the list is `config/movie assets` and the second is `config/tv assets`. - -4. Lists in YAML are represented by using the hyphen (-) and space. They are ordered and can be embedded inside a map using indentation. - - ```{ .yaml .no-copy } - asset_directory: - - config/movie assets - - config/tv assets - ``` - - The first item in the list is `config/movie assets` and the second is `config/tv assets`. \ No newline at end of file diff --git a/docs/templates/tables/collection_order.md b/docs/templates/tables/collection_order.md index dfdf76102..53e16bf8b 100644 --- a/docs/templates/tables/collection_order.md +++ b/docs/templates/tables/collection_order.md @@ -8,7 +8,7 @@ </tr><tr> <td>`custom`</td><td>Order Collection Via the Builder Order</td> </tr><tr> - <td>[Any `plex_search` Sort Option](../../files/builders/plex.md#sort-options)</td> + <td>[Any `plex_search` Sort Option](../../files/builders/plex/sort-options.md)</td> <td>Order Collection by any `plex_search` Sort Option</td> </tr> </table> \ No newline at end of file diff --git a/docs/templates/variable_list.md b/docs/templates/variable_list.md index 0b3ddd970..0ab289544 100644 --- a/docs/templates/variable_list.md +++ b/docs/templates/variable_list.md @@ -1,5 +1,5 @@ <!--aspect-overlay--> -| `text_<<key>>`<sup>**1**</sup> | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`1.33`</td><td>`1.33`</td></tr><tr><td>`1.65`</td><td>`1.65`</td></tr><tr><td>`1.66`</td><td>`1.66`</td></tr><tr><td>`1.78`</td><td>`1.78`</td></tr><tr><td>`1.85`</td><td>`1.85`</td></tr><tr><td>`2.2`</td><td>`2.2`</td></tr><tr><td>`2.35`</td><td>`2.35`</td></tr><tr><td>`2.77`</td><td>`2.77`</td></tr></table> | +| `text_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`1.33`</td><td>`1.33`</td></tr><tr><td>`1.65`</td><td>`1.65`</td></tr><tr><td>`1.66`</td><td>`1.66`</td></tr><tr><td>`1.78`</td><td>`1.78`</td></tr><tr><td>`1.85`</td><td>`1.85`</td></tr><tr><td>`2.2`</td><td>`2.2`</td></tr><tr><td>`2.35`</td><td>`2.35`</td></tr><tr><td>`2.77`</td><td>`2.77`</td></tr></table> | <!--aspect-overlay--> <!--commonsense-overlay--> | `pre_text` | **Description:** Choose the text before the key for the Overlay.<br>**Values:** Any String | @@ -12,7 +12,7 @@ | `use_subtitles` | **Description:** Controls if the overlay is based on subtitle language instead of audio language.<br>**Values:** `true` to look at subtitle language instead of audio language | <!--language_count-overlay--> <!--languages-overlay--> -| `country_<<key>>`<sup>**1**</sup> | **Description:** Controls the country image for the Overlay.<br>**Default:** Listed in the [Table](#supported-audiosubtitle-language-flags) above<br>**Values:** [ISO 3166-1 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) for the flag desired | +| `country_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the country image for the Overlay.<br>**Default:** Listed in the [Table](#supported-audiosubtitle-language-flags) above<br>**Values:** [ISO 3166-1 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) for the flag desired | | `flag_alignment` | **Description:** Controls the flag alignment in the backdrop.<br>**Default:** `left`<br>**Values:** `left` or `right` | | `group_alignment` | **Description:** Choose the display alignment for the flag group.<br>**Default:** `vertical`<br>**Values:** `horizontal`, or `vertical` | | `hide_text` | **Description:** Disables the country code text, showing only the flag.<br>**Default: `false` <br>**Values:\*\* `true` to hide the country text | @@ -22,10 +22,10 @@ | `initial_horizontal_offset` | **Description:** Controls the initial horizontal offset the queue starts from.<br>**Values:** Any Integer | | `initial_vertical_align` | **Description:** Controls the initial vertical align the queue starts from.<br>**Values:** `top`, `center`, or `bottom` | | `initial_vertical_offset` | **Description:** Controls the initial vertical offset the queue starts from.<br>**Values:** Any Integer | -| `languages` | **Description:** Controls which Languages will be active.<br>**Default:** `["en", "de", "fr", "es", "pt", "ja"]` <br>**Values:** List of [ISO 639-1 Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for the Languages desired | +| `languages` | **Description:** Controls which Languages will be active.<br>**Default:** `["en", "de", "fr", "es", "pt", "ja"]` <br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of [ISO 639-1 Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for the Languages desired | | `offset` | **Description:** Controls the offset between the flag and the text.<br>**Default:** `10`<br>**Values:** Any Integer 0 or greater | | `overlay_limit` | **Description:** Choose the number of overlay this queue displays.<br>**Default:** `3`<br>**Values:** `1`, `2`, `3`, `4`, or `5` | -| `position` | **Description:** Use the Custom Given Queue instead of the the provided Queues.<br>**Values:** List of Coordinates | +| `position` | **Description:** Use the Custom Given Queue instead of the the provided Queues.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Coordinates | | `size` | **Description:** Controls the size of the overlay.<br>**Default: small** <br>**Values:** `small` or `big` | | `style` | **Description:** Controls the visual theme of the overlays created.<table class="clearTable"><tr><th>Values:</th></tr><tr><td><code>round</code></td><td>Round Theme</td></tr><tr><td><code>square</code></td><td>Square Theme</td></tr><tr><td><code>half</code></td><td>Square Flag with Round Background</td></tr></table> | | `use_lowercase` | **Description:** Controls if the overlay display is in lowercase.<br>**Values:** `true` to use lowercase text | @@ -74,7 +74,7 @@ <!--runtimes-overlay--> <!--status-overlay--> | `last` | **Description:** Episode Air Date in the last number of days for the AIRING Overlay.<br>**Default:** `14`<br>**Values:** Any number greater than 0 | -| `text_<<key>>`<sup>**1**</sup> | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`airing`</td><td>`AIRING`</td></tr><tr><td>`returning`</td><td>`RETURNING`</td></tr><tr><td>`canceled`</td><td>`CANCELED`</td></tr><tr><td>`ended`</td><td>`ENDED`</td></tr></table> | +| `text_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`airing`</td><td>`AIRING`</td></tr><tr><td>`returning`</td><td>`RETURNING`</td></tr><tr><td>`canceled`</td><td>`CANCELED`</td></tr><tr><td>`ended`</td><td>`ENDED`</td></tr></table> | <!--status-overlay--> <!--streaming-overlay--> | `discover_with_<<key>>` | **Description:** Overrides the TMDb Watch Provider used for the specified key. This is only needed if a specific `region` has a different ID for the watch provider.<br>**Default:** `<<discover_with>>`<br>**Values:** Any TMDb Watch Provider ID for [Movies](https://developer.themoviedb.org/reference/watch-providers-movie-list) / [Shows](https://developer.themoviedb.org/reference/watch-provider-tv-list) based on the user's region | @@ -85,42 +85,42 @@ | `style` | **Description:** Choose between the standard size or the **bigger** one.<br>**Values:** `bigger` | <!--studio-overlay--> <!--video_format-overlay--> -| `text_<<key>>`<sup>**1**</sup> | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`remux`</td><td>`REMUX`</td></tr><tr><td>`bluray`</td><td>`BLU-RAY`</td></tr><tr><td>`web`</td><td>`WEB`</td></tr><tr><td>`hdtv`</td><td>`HDTV`</td></tr><tr><td>`dvd`</td><td>`DVD`</td></tr><tr><td>`sdtv`</td><td>`SDTV`</td></tr></table> | +| `text_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Choose the text for the Overlay.<br><br>**Values:** Any String<br><table class="clearTable" style="text-align:left;"><tr><th>Key</th><th>Default Text</th></tr><tr><td>`remux`</td><td>`REMUX`</td></tr><tr><td>`bluray`</td><td>`BLU-RAY`</td></tr><tr><td>`web`</td><td>`WEB`</td></tr><tr><td>`hdtv`</td><td>`HDTV`</td></tr><tr><td>`dvd`</td><td>`DVD`</td></tr><tr><td>`sdtv`</td><td>`SDTV`</td></tr></table> | <!--video_format-overlay--> <!--playlists--> -| `delete_playlist_<<key>>`<sup>**1**</sup> | **Description:** Will delete the key's playlists for the users defined by sync_to_users.<br>**Values:** `true` or `false` | +| `delete_playlist_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Will delete the key's playlists for the users defined by sync_to_users.<br>**Values:** `true` or `false` | | `delete_playlist` | **Description:** Will delete all playlists for the users defined by sync_to_users.<br>**Values:** `true` or `false` | -| `exclude_user_<<key>>`<sup>**1**</sup> | **Description:** Sets the users to exclude from sync the key's playlist.<br>**Default:** `sync_to_users` Value<br>**Values:** Comma-separated string or list of user names. | +| `exclude_user_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the users to exclude from sync the key's playlist.<br>**Default:** `sync_to_users` Value<br>**Values:** Comma-separated string or list of user names. | | `exclude_user` | **Description:** Sets the users to exclude from sync for all playlists.<br>**Default:** `playlist_sync_to_users` Global Setting Value<br>**Values:** Comma-separated string or list of user names. | -| `ignore_ids` | **Description:** Set a list or comma-separated string of TMDb/TVDb IDs to ignore in all playlists.<br>**Values:** List or comma-separated string of TMDb/TVDb IDs | -| `ignore_imdb_ids` | **Description:** Set a list or comma-separated string of IMDb IDs to ignore in all playlists.<br>**Values:** List or comma-separated string of IMDb IDs | -| `imdb_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the IMDb List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List of Trakt List URLs | -| `item_radarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr of the key's playlist.<br>**Default:** `item_radarr_tag`<br>**Values:** List or comma-separated string of tags | -| `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr for all playlists in a Defaults file.<br>**Values:** List or comma-separated string of tags | -| `item_sonarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr of the key's playlist.<br>**Default:** `item_sonarr_tag`<br>**Values:** List or comma-separated string of tags | -| `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr for all playlists in a Defaults file.<br>**Values:** List or comma-separated string of tags | +| `ignore_ids` | **Description:** Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs to ignore in all playlists.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of TMDb/TVDb IDs | +| `ignore_imdb_ids` | **Description:** Set a List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs to ignore in all playlists.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of IMDb IDs | +| `imdb_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the IMDb List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt List URLs | +| `item_radarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr of the key's playlist.<br>**Default:** `item_radarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_radarr_tag` | **Description:** Used to append a tag in Radarr for every movie found by the builders that's in Radarr for all playlists in a Defaults file.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_sonarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr of the key's playlist.<br>**Default:** `item_sonarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_sonarr_tag` | **Description:** Used to append a tag in Sonarr for every series found by the builders that's in Sonarr for all playlists in a Defaults file.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | | `libraries` | **Description:** Sets the names of the libraries to use for the Playlists.<br>**Default:** `Movies, TV Shows`<br>**Values:** Comma-separated string or list of library mapping names defined in the `libraries` attribute in the base of your [Configuration File](../config/overview.md. | -| `mdblist_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the MDBList List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List of Trakt List URLs | -| `name_<<key>>`<sup>**1**</sup> | **Description:** Changes the name of the key's playlist.<br>**Values:** New Playlist Name | -| `radarr_add_missing_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `add_missing` attribute of the key's playlist.<br>**Default:** `radarr_add_missing`<br>**Values:** `true` or `false` | +| `mdblist_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the MDBList List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt List URLs | +| `name_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the name of the key's playlist.<br>**Values:** New Playlist Name | +| `radarr_add_missing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `add_missing` attribute of the key's playlist.<br>**Default:** `radarr_add_missing`<br>**Values:** `true` or `false` | | `radarr_add_missing` | **Description:** Override Radarr `add_missing` attribute for all playlists in a Defaults file.<br>**Values:** `true` or `false` | -| `radarr_folder_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `root_folder_path` attribute of the key's playlist.<br>**Default:** `radarr_folder`<br>**Values:** Folder Path | +| `radarr_folder_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `root_folder_path` attribute of the key's playlist.<br>**Default:** `radarr_folder`<br>**Values:** Folder Path | | `radarr_folder` | **Description:** Override Radarr `root_folder_path` attribute for all playlists in a Defaults file.<br>**Values:** Folder Path | -| `radarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Override Radarr `tag` attribute of the key's playlist.<br>**Default:** `radarr_tag`<br>**Values:** List or comma-separated string of tags | -| `radarr_tag` | **Description:** Override Radarr `tag` attribute for all playlists in a Defaults file.<br>**Values:** List or comma-separated string of tags | -| `sonarr_add_missing_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `add_missing` attribute of the key's playlist.<br>**Default:** `sonarr_add_missing`<br>**Values:** `true` or `false` | +| `radarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Radarr `tag` attribute of the key's playlist.<br>**Default:** `radarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `radarr_tag` | **Description:** Override Radarr `tag` attribute for all playlists in a Defaults file.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `sonarr_add_missing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `add_missing` attribute of the key's playlist.<br>**Default:** `sonarr_add_missing`<br>**Values:** `true` or `false` | | `sonarr_add_missing` | **Description:** Override Sonarr `add_missing` attribute for all playlists in a Defaults file.<br>**Values:** `true` or `false` | -| `sonarr_folder_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `root_folder_path` attribute of the key's playlist.<br>**Default:** `sonarr_folder`<br>**Values:** Folder Path | +| `sonarr_folder_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `root_folder_path` attribute of the key's playlist.<br>**Default:** `sonarr_folder`<br>**Values:** Folder Path | | `sonarr_folder` | **Description:** Override Sonarr `root_folder_path` attribute for all playlists in a Defaults file.<br>**Values:** Folder Path | -| `sonarr_tag_<<key>>`<sup>**1**</sup> | **Description:** Override Sonarr `tag` attribute of the key's playlist.<br>**Default:** `sonarr_tag`<br>**Values:** List or comma-separated string of tags | -| `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all playlists in a Defaults file.<br>**Values:** List or comma-separated string of tags | -| `summary_<<key>>`<sup>**1**</sup> | **Description:** Changes the summary of the key's playlist.<br>**Values:** New Playlist Summary | -| `sync_to_users_<<key>>`<sup>**1**</sup> | **Description:** Sets the users to sync the key's playlist to.<br>**Default:** `sync_to_user` Value<br>**Values:** Comma-separated string or list of user names. | +| `sonarr_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override Sonarr `tag` attribute of the key's playlist.<br>**Default:** `sonarr_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `sonarr_tag` | **Description:** Override Sonarr `tag` attribute for all playlists in a Defaults file.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `summary_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the summary of the key's playlist.<br>**Values:** New Playlist Summary | +| `sync_to_users_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the users to sync the key's playlist to.<br>**Default:** `sync_to_user` Value<br>**Values:** Comma-separated string or list of user names. | | `sync_to_users` | **Description:** Sets the users to sync all playlists to.<br>**Default:** `playlist_sync_to_users` Global Setting Value<br>**Values:** Comma-separated string or list of user names. | -| `trakt_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the Trakt List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List of Trakt List URLs | -| `url_poster_<<key>>`<sup>**1**</sup> | **Description:** Changes the poster url of the key's playlist.<br>**Values:** URL directly to the Image | -| `use_<<key>>`<sup>**1**</sup> | **Description:** Turns off individual Playlists in a Defaults file.<br>**Values:** `false` to turn off the playlist | +| `trakt_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the Trakt List to the key's playlist. Overrides the [default trakt_list] for that playlist if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt List URLs | +| `url_poster_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the poster url of the key's playlist.<br>**Values:** URL directly to the Image | +| `use_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Turns off individual Playlists in a Defaults file.<br>**Values:** `false` to turn off the playlist | <!--playlists--> <!--addon_image--> @@ -135,13 +135,13 @@ <!--color--> <!--regex--> -| `regex_<<key>>`<sup>**1**</sup> | **Description:** Controls the regex of the Overlay Search.<br>**Values:** Any Proper Regex | +| `regex_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the regex of the Overlay Search.<br>**Values:** Any Proper Regex | <!--regex--> <!--style--> | `style` | **Description:** Choose the Overlay Style.<br>**Default:** `compact`<br>**Values:** `compact` or `standard` | <!--style--> <!--weight--> -| `weight_<<key>>`<sup>**1**</sup> | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number | +| `weight_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the weight of the Overlay. Higher numbers have priority.<br>**Values:** Any Number | <!--weight--> <!--overlay-white-style--> | `style` | **Description:** Choose between the default color version or the **white** one.<br>**Values:** `color` or `white` | @@ -160,74 +160,74 @@ <!--award--> <!--seasonal--> | `emoji` | **Description:** Prefix an emoji to the title of Collections. <br>**Values:** Any emoji followed by a space, all wrapped in quotes (i.e. "🎅 ") | -| `emoji_<<key>>`<sup>**1**</sup> | **Description:** Prefix an emoji to the title of the specified [key's](#collection_section) collection. Overrides the [default emoji](#default-values) for that collection if used.<br>**Values:** Any emoji followed by a space, all wrapped in quotes (i.e. "🔥 ") | -| `imdb_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the IMDb List to the specified [key's](#collection_section) collection.<br>**Values:** List of IMDb List URLs | -| `imdb_search_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the IMDb Search to the specified [key's](#collection_section) collection. Overrides the [default imdb_search](#default-values) for that collection if used.<br>**Values:** List of IMDb List URLs | -| `letterboxd_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the Letterboxd List to the specified [key's](#collection_section) collection.<br>**Values:** List of Letterboxd List URLs | -| `mdblist_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the MDb List to the specified [key's](#collection_section) collection. Overrides the [default mdblist_list](#default-values) for that collection if used.<br>**Values:** List of MDBList URLs | -| `tmdb_collection_<<key>>`<sup>**1**</sup> | **Description:** Adds the TMDb Collection IDs given to the specified [key's](#collection_section) collection. Overrides the [default tmdb_collection](#default-values) for that collection if used.<br>**Values:** List of TMDb Collection IDs | -| `tmdb_movie_<<key>>`<sup>**1**</sup> | **Description:** Adds the TMDb Movie IDs given to the specified [key's](#collection_section) collection. Overrides the [default tmdb_movie](#default-values) for that collection if used.<br>**Values:** List of TMDb Movie IDs | -| `trakt_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the Trakt List to the specified [key's](#collection_section) collection. Overrides the [default trakt_list](#default-values) for that collection if used.<br>**Values:** List of Trakt List URLs | +| `emoji_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Prefix an emoji to the title of the specified [key's](#collection_section) collection. Overrides the [default emoji](#default-value-source) for that collection if used.<br>**Values:** Any emoji followed by a space, all wrapped in quotes (i.e. "🔥 ") | +| `imdb_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the IMDb List to the specified [key's](#collection_section) collection.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of IMDb List URLs | +| `imdb_search_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the IMDb Search to the specified [key's](#collection_section) collection. Overrides the [default imdb_search](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of IMDb List URLs | +| `letterboxd_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the Letterboxd List to the specified [key's](#collection_section) collection.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Letterboxd List URLs | +| `mdblist_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the MDb List to the specified [key's](#collection_section) collection. Overrides the [default mdblist_list](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of MDBList URLs | +| `tmdb_collection_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the TMDb Collection IDs given to the specified [key's](#collection_section) collection. Overrides the [default tmdb_collection](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Collection IDs | +| `tmdb_movie_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the TMDb Movie IDs given to the specified [key's](#collection_section) collection. Overrides the [default tmdb_movie](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Movie IDs | +| `trakt_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the Trakt List to the specified [key's](#collection_section) collection. Overrides the [default trakt_list](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt List URLs | | `schedule` | **Description:** Changes the Schedule for all collections in this file. Use `daily` to have all collections show.<br>**Values:** [Any Schedule Option](../../config/schedule.md) | -| `schedule_<<key>>`<sup>**1**</sup> | **Description:** Changes the Schedule of the specified [key's](#collection_section) collection. Overrides the [default schedule](#default-values) for that collection if used.<br>**Values:** [Any Schedule Option](../../config/schedule.md) | +| `schedule_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Schedule of the specified [key's](#collection_section) collection. Overrides the [default schedule](#default-value-source) for that collection if used.<br>**Values:** [Any Schedule Option](../../config/schedule.md) | <!--seasonal--> <!--show-franchise--> -| `addons` | **Description:** Overrides the [default addons dictionary](#default-values). Defines how multiple keys can be combined under a parent key. The parent key doesn't have to already exist in Plex<br>**Values:** Dictionary List of TMDb Show IDs | -| `append_addons` | **Description:** Appends to the [default addons dictionary](#default-values).<br>**Values:** Dictionary List of TMDb Show IDs | -| `append_data` | **Description:** Appends to the [default data dictionary](#default-values).<br>**Values:** Dictionary List of TMDb Main Show ID | +| `addons` | **Description:** Overrides the [default addons dictionary](#default-value-source). Defines how multiple keys can be combined under a parent key. The parent key doesn't have to already exist in Plex<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Show IDs | +| `append_addons` | **Description:** Appends to the [default addons dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Show IDs | +| `append_data` | **Description:** Appends to the [default data dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Main Show ID | | `build_collection` | **Description:** Controls if you want the collection to actually be built. i.e. you may just want these shows sent to Sonarr.<br>**Values:** `false` to not build the collection | | `collection_section` | **Description:** Adds a sort title with this collection sections.<br>**Values:** Any number | -| `data` | **Description:** Overrides the [default data dictionary](#default-values). Defines the data that the custom dynamic collection processes.<br>**Values:** Dictionary List of TMDb Main Show ID | -| `exclude` | **Description:** Exclude these Collections from creating a Dynamic Collection.<br>**Values:** List of Collection IDs | +| `data` | **Description:** Overrides the [default data dictionary](#default-value-source). Defines the data that the custom dynamic collection processes.<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Main Show ID | +| `exclude` | **Description:** Exclude these Collections from creating a Dynamic Collection.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Collection IDs | | `minimum_items` | **Description:** Controls the minimum items that the collection must have to be created.<br>**Default:** `2`<br>**Values:** Any number | -| `name_mapping_<<key>>`<sup>**1**</sup> | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection.<br>**Values:** Any String | -| `order_<<key>>`<sup>**1**</sup> | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | -| `remove_addons` | **Description:** Removes from the [default addons dictionary](#default-values).<br>**Values:** Dictionary List of TMDb Show IDs | -| `remove_data` | **Description:** Removes from the [default data dictionary](#default-values).<br>**Values:** List of TMDb Main Show IDs to remove | -| `sort_title_<<key>>`<sup>**1**</sup> | **Description:** Sets the sort title of the [key's](#collection_section) collection.<br>**Default:** `sort_title`<br>**Values:** Any String | +| `name_mapping_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection.<br>**Values:** Any String | +| `order_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | +| `remove_addons` | **Description:** Removes from the [default addons dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Show IDs | +| `remove_data` | **Description:** Removes from the [default data dictionary](#default-value-source).<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Main Show IDs to remove | +| `sort_title_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the sort title of the [key's](#collection_section) collection.<br>**Default:** `sort_title`<br>**Values:** Any String | | `sort_title` | **Description:** Sets the sort title for all collections. Use `<<collection_name>>` to use the collection name. **Example:** `"!02_<<collection_name>>"`<br>**Values:** Any String with `<<collection_name>>` | -| `summary_<<key>>`<sup>**1**</sup> | **Description:** Changes the summary of the [key's](#collection_section) collection.<br>**Values:** New Collection Summary | +| `summary_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the summary of the [key's](#collection_section) collection.<br>**Values:** New Collection Summary | <!--show-franchise--> <!--franchise--> | `build_collection` | **Description:** Controls if you want the collection to actually be built. i.e. you may just want these movies sent to Radarr.<br>**Values:** `false` to not build the collection | | `collection_section` | **Description:** Adds a sort title with this collection sections.<br>**Values:** Any number | | `minimum_items` | **Description:** Controls the minimum items that the collection must have to be created.<br>**Default:** `2`<br>**Values:** Any number | -| `movie_<<key>>`<sup>**1**</sup> | **Description:** Adds the TMDb Movie IDs given to the [key's](#collection_section) collection. Overrides the [default movie](#default-values) for that collection if used.<br>**Values:** List of TMDb Movie IDs | -| `name_mapping_<<key>>`<sup>**1**</sup> | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection.Overrides the [default name_mapping](#default-values) for that collection if used.<br>**Values:** Any String | -| `order_<<key>>`<sup>**1**</sup> | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | -| `sort_title_<<key>>`<sup>**1**</sup> | **Description:** Sets the sort title of the [key's](#collection_section) collection.<br>**Default:** `sort_title`<br>**Values:** Any String | +| `movie_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the TMDb Movie IDs given to the [key's](#collection_section) collection. Overrides the [default movie](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of TMDb Movie IDs | +| `name_mapping_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection.Overrides the [default name_mapping](#default-value-source) for that collection if used.<br>**Values:** Any String | +| `order_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Controls the sort order of the collections in their collection section.<br>**Values:** Any number | +| `sort_title_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the sort title of the [key's](#collection_section) collection.<br>**Default:** `sort_title`<br>**Values:** Any String | | `sort_title` | **Description:** Sets the sort title for all collections. Use `<<collection_name>>` to use the collection name. **Example:** `"!02_<<collection_name>>"`<br>**Values:** Any String with `<<collection_name>>` | -| `summary_<<key>>`<sup>**1**</sup> | **Description:** Changes the summary of the [key's](#collection_section) collection.<br>**Values:** New Collection Summary | -| `title_override` | **Description:** Overrides the [default title_override dictionary](#default-values).<br>**Values:** Dictionary with `key: new_title` entries | +| `summary_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the summary of the [key's](#collection_section) collection.<br>**Values:** New Collection Summary | +| `title_override` | **Description:** Overrides the [default title_override dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } with `key: new_title` entries | <!--franchise--> <!--arr--> -| `ARR_CODE_add_missing_<<key>>`<sup>**1**</sup> | **Description:** Override ARR_NAME `add_missing` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_add_missing`<br>**Values:** `true` or `false` | +| `ARR_CODE_add_missing_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override ARR_NAME `add_missing` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_add_missing`<br>**Values:** `true` or `false` | | `ARR_CODE_add_missing` | **Description:** Override ARR_NAME `add_missing` attribute for all collections in a Defaults File.<br>**Values:** `true` or `false` | -| `ARR_CODE_folder_<<key>>`<sup>**1**</sup> | **Description:** Override ARR_NAME `root_folder_path` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_folder`<br>**Values:** Folder Path | +| `ARR_CODE_folder_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override ARR_NAME `root_folder_path` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_folder`<br>**Values:** Folder Path | | `ARR_CODE_folder` | **Description:** Override ARR_NAME `root_folder_path` attribute for all collections in a Defaults File.<br>**Values:** Folder Path | -| `ARR_CODE_tag_<<key>>`<sup>**1**</sup> | **Description:** Override ARR_NAME `tag` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_tag`<br>**Values:** List or comma-separated string of tags | -| `ARR_CODE_tag` | **Description:** Override ARR_NAME `tag` attribute for all collections in a Defaults File.<br>**Values:** List or comma-separated string of tags | -| `item_ARR_CODE_tag_<<key>>`<sup>**1**</sup> | **Description:** Used to append a tag in ARR_NAME for every ARR_TYPE found by the builders that's in ARR_NAME of the [key's](#collection_section) collection.<br>**Default:** `item_ARR_CODE_tag`<br>**Values:** List or comma-separated string of tags | -| `item_ARR_CODE_tag` | **Description:** Used to append a tag in ARR_NAME for every ARR_TYPE found by the builders that's in ARR_NAME for all collections in a Defaults File.<br>**Values:** List or comma-separated string of tags | +| `ARR_CODE_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Override ARR_NAME `tag` attribute of the [key's](#collection_section) collection.<br>**Default:** `ARR_CODE_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `ARR_CODE_tag` | **Description:** Override ARR_NAME `tag` attribute for all collections in a Defaults File.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_ARR_CODE_tag_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Used to append a tag in ARR_NAME for every ARR_TYPE found by the builders that's in ARR_NAME of the [key's](#collection_section) collection.<br>**Default:** `item_ARR_CODE_tag`<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | +| `item_ARR_CODE_tag` | **Description:** Used to append a tag in ARR_NAME for every ARR_TYPE found by the builders that's in ARR_NAME for all collections in a Defaults File.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } or comma-separated string of tags | <!--arr--> <!--basic--> -| `in_the_last_<<key>>`<sup>**1**</sup> | **Description:** Changes how far back the Smart Filter looks.<br>**Default:**<table class="clearTable"><tr><td>Key</td><td>Value</td></tr><tr><td>`released`</td><td>`90`</td></tr><tr><td>`episodes`</td><td>`7`</td></tr></table><br>**Values:** Number greater than 0 | +| `in_the_last_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes how far back the Smart Filter looks.<br>**Default:**<table class="clearTable"><tr><td>Key</td><td>Value</td></tr><tr><td>`released`</td><td>`90`</td></tr><tr><td>`episodes`</td><td>`7`</td></tr></table><br>**Values:** Number greater than 0 | <!--basic--> <!--myanimelist--> -| `starting_only` | **Description:** Changes the season collection to only use anime listed under the new section on [MAL Seasons](https://myanimelist.net/anime/season/)<br>**Default:** `False`<br>**Values:** `True` or `False` | +| `starting_only` | **Description:** Changes the season collection to only use anime listed under the new section on [MAL Seasons](https://myanimelist.net/anime/season)<br>**Default:** `False`<br>**Values:** `True` or `False` | <!--myanimelist--> <!--tautulli--> | `list_days` | **Description:** Changes the `list_days` attribute of the Builder for all collections in a Defaults File.<br>**Values:** Number greater than 0 | -| `list_days_<<key>>`<sup>**1**</sup> | **Description:** Changes the `list_days` attribute of the Builder of the [key's](#collection_section) collection.<br>**Values:** Number greater than 0 | +| `list_days_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the `list_days` attribute of the Builder of the [key's](#collection_section) collection.<br>**Values:** Number greater than 0 | | `list_size` | **Description:** Changes the `list_size` attribute of the Builder for all collections in a Defaults File.<br>**Values:** Number greater than 0 | -| `list_size_<<key>>`<sup>**1**</sup> | **Description:** Changes the `list_size` attribute of the Builder of the [key's](#collection_section) collection.<br>**Values:** Number greater than 0 | +| `list_size_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the `list_size` attribute of the Builder of the [key's](#collection_section) collection.<br>**Values:** Number greater than 0 | <!--tautulli--> <!--universe--> -| `name_mapping_<<key>>`<sup>**1**</sup> | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection. <br>**Values:** Any String | +| `name_mapping_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Sets the name mapping value for using assets of the [key's](#collection_section) collection. <br>**Values:** Any String | | `minimum_items` | **Description:** Controls the minimum items that the collection must have to be created.<br>**Default:** `2`<br>**Values:** Any number | -| `trakt_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the Trakt List to the [key's](#collection_section) collection. Overrides the [default trakt_url](#default-values) for that collection if used.<br>**Values:** List of Trakt List URLs | -| `imdb_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the IMDb List to the [key's](#collection_section) collection.<br>**Values:** List of IMDb List URLs | -| `mdblist_list_<<key>>`<sup>**1**</sup> | **Description:** Adds the Movies in the MDBList List to the [key's](#collection_section) collection. Overrides the [default mdblist_url](#default-values) for that collection if used.<br>**Values:** List of MDBList List URLs | +| `trakt_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the Trakt List to the [key's](#collection_section) collection. Overrides the [default trakt_url](#default-value-source) for that collection if used.<br>**Values:** List :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-lists" } of Trakt List URLs | +| `imdb_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the IMDb List to the [key's](#collection_section) collection.<br>**Values:** List of IMDb List URLs | +| `mdblist_list_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Adds the Movies in the MDBList List to the [key's](#collection_section) collection. Overrides the [default mdblist_url](#default-value-source) for that collection if used.<br>**Values:** List of MDBList List URLs | <!--universe--> <!--streaming--> | `discover_with_<<key>>` | **Description:** Overrides the TMDb Watch Provider used for the specified key. This is only needed if a specific `region` has a different ID for the watch provider.<br>**Default:** `<<discover_with>>`<br>**Values:** Any TMDb Watch Provider ID for [Movies](https://developer.themoviedb.org/reference/watch-providers-movie-list) / [Shows](https://developer.themoviedb.org/reference/watch-provider-tv-list) based on the user's region | @@ -244,7 +244,8 @@ | `summary_collectionless` | **Description:** Changes the summary of the collection.<br>**Values:** New Collection Summary | <!--collectionless--> <!--people-data--> -| `data` | **Description:** Replaces the `data` dynamic collection value.<br><table class="clearTable"><tr><th>Attribute</th><th>Description & Values</th></tr><tr><td><code>depth</code></td><td>Controls the depth within the casting credits to search for common actors<br><strong>Default:</strong> 5<br><strong>Values:</strong> Number greater than 0</td></tr><tr><td><code>limit</code></td><td>Controls the maximum number of collections to create<br><strong>Default:</strong> 25<br><strong>Values:</strong> Number greater than 0</td></tr></table> | +| `data` | **Description:** Replaces the `data` dynamic collection value.<br><table class="clearTable"><tr><th>Attribute</th><th>Description & Values</th></tr><tr><td><code>depth</code></td><td>Controls the depth within the casting credits to search for common actors<br><strong>Default:</strong> 5<br><strong>Values:</strong> Number greater than 0</td></tr><tr><td><code>limit</code></td><td>Controls the maximum number of collections to create<br><strong>Default:</strong> 25<br><strong>Values:</strong> Number greater than 0</td></tr></table> | +| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | <!--people-data--> <!--award-data--> | `data` | **Description:** Replaces the `data` dynamic collection value.<br><table class="clearTable"><tr><th>Attribute</th><th>Description & Values</th></tr><tr><td><code>starting</code></td><td>Controls the starting year for collections<br><strong>Default:</strong> latest-5<br><strong>Values:</strong> Number greater than 0</td></tr><tr><td><code>ending</code></td><td>Controls the ending year for collections<br><strong>Default:</strong> latest<br><strong>Values:</strong> Number greater than 1</td></tr><tr><td><code>increment</code></td><td>Controls the increment (i.e. every 5th year)<br><strong>Default:</strong> 1<br><strong>Values:</strong> Number greater than 0</td><td></td></tr></table><ul><li><strong><code>starting</code> and <code>ending</code> can also have the value <code>latest</code></strong></li><li><strong>You can also use a value relative to the <code>latest</code> by doing <code>latest-5</code></strong></li></ul> | @@ -253,16 +254,16 @@ | `data` | **Description:** Replaces the `data` dynamic collection value.<br><table class="clearTable"><tr><th>Attribute</th><th>Description & Values</th></tr><tr><td><code>starting</code></td><td>Controls the starting year for collections<br><strong>Default:</strong> current_year-10<br><strong>Values:</strong> Number greater than 0</td></tr><tr><td><code>ending</code></td><td>Controls the ending year for collections<br><strong>Default:</strong> current_year<br><strong>Values:</strong> Number greater than 1</td></tr><tr><td><code>increment</code></td><td>Controls the increment (i.e. every 5th year)<br><strong>Default:</strong> 1<br><strong>Values:</strong> Number greater than 0</td><td></td></tr></table><ul><li><strong><code>starting</code> and <code>ending</code> can also have the value <code>current_year</code></strong></li><li><strong>You can also use a value relative to the <code>current_year</code> by doing <code>current_year-5</code></strong></li></ul> | <!--year-data--> <!--data--> -| `data` | **Description:** Overrides the [default data dictionary](#default-values). Defines the data that the custom dynamic collection processes.<br>**Values:** Dictionary List of keys/names | -| `append_data` | **Description:** Appends to the [default data dictionary](#default-values).<br>**Values:** Dictionary List of keys/names | -| `remove_data` | **Description:** Removes from the [default data dictionary](#default-values).<br>**Values:** List of keys to remove | +| `data` | **Description:** Overrides the [default data dictionary](#default-value-source). Defines the data that the custom dynamic collection processes.<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List of keys/names | +| `append_data` | **Description:** Appends to the [default data dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List of keys/names | +| `remove_data` | **Description:** Removes from the [default data dictionary](#default-value-source).<br>**Values:** List of keys to remove | <!--data--> <!--addons--> -| `addons` | **Description:** Overrides the [default addons dictionary](#default-values). Defines how multiple keys can be combined under a parent key. The parent key doesn't have to already exist in Plex<br>**Values:** Dictionary List of DYNAMIC_VALUE | +| `addons` | **Description:** Overrides the [default addons dictionary](#default-value-source). Defines how multiple keys can be combined under a parent key. The parent key doesn't have to already exist in Plex<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List of DYNAMIC_VALUE | <!--addons--> <!--addons-extra--> -| `append_addons` | **Description:** Appends to the [default addons dictionary](#default-values).<br>**Values:** Dictionary List of DYNAMIC_VALUE | -| `remove_addons` | **Description:** Removes from the [default addons dictionary](#default-values).<br>**Values:** Dictionary List of DYNAMIC_VALUE | +| `append_addons` | **Description:** Appends to the [default addons dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List of DYNAMIC_VALUE | +| `remove_addons` | **Description:** Removes from the [default addons dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } List of DYNAMIC_VALUE | <!--addons-extra--> <!--exclude--> | `exclude` | **Description:** Exclude these DYNAMIC_NAME from creating a Dynamic Collection.<br>**Values:** List of DYNAMIC_VALUE | @@ -271,11 +272,11 @@ | `include` | **Description:** Force these NAME to be included to create a Dynamic Collection.<br>**Values:** List of DYNAMIC_VALUE | <!--include--> <!--include-extra--> -| `append_include` | **Description:** Appends to the [default include list](#default-values)<br>**Values:** List of DYNAMIC_VALUE | -| `remove_include` | **Description:** Removes from the [default include list](#default-values)<br>**Values:** List of DYNAMIC_VALUE | +| `append_include` | **Description:** Appends to the [default include list](#default-value-source)<br>**Values:** List of DYNAMIC_VALUE | +| `remove_include` | **Description:** Removes from the [default include list](#default-value-source)<br>**Values:** List of DYNAMIC_VALUE | <!--include-extra--> <!--key_name_override--> -| `key_name_override` | **Description:** Overrides the [default key_name_override dictionary](#default-values).<br>**Values:** Dictionary with `key: new_key_name` entries | +| `key_name_override` | **Description:** Overrides the [default key_name_override dictionary](#default-value-source).<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } with `key: new_key_name` entries | <!--key_name_override--> <!--cache_builders--> | `cache_builders` | **Description:** Changes the Builder Cache for all collections in a Defaults File.<br>**Default:** `1`<br>**Values:** number 0 or greater | @@ -286,22 +287,19 @@ <!--collection_mode--> <!--collection_order--> | `collection_order` | **Description:** Changes the Collection Order for all collections in a Defaults File.<br>**Default:** COLLECTION_ORDER<br>**Values:**{% include-markdown "./tables/collection_order.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | -| `collection_order_<<key>>`<sup>**1**</sup> | **Description:** Changes the Collection Order of the [key's](#collection_section) collection.<br>**Default:** `collection_order`<br>**Values:**{% include-markdown "./tables/collection_order.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | +| `collection_order_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Collection Order of the [key's](#collection_section) collection.<br>**Default:** `collection_order`<br>**Values:**{% include-markdown "./tables/collection_order.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | <!--collection_order--> <!--limit--> | `limit` | **Description:** Changes the Builder Limit for all collections in a Defaults File.<!--limit-extra--><br>**Values:** Number Greater than 0 | -| `limit_<<key>>`<sup>**1**</sup> | **Description:** Changes the Builder Limit of the [key's](#collection_section) collection.<br>**Default:** `limit`<br>**Values:** Number Greater than 0 | +| `limit_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Builder Limit of the [key's](#collection_section) collection.<br>**Default:** `limit`<br>**Values:** Number Greater than 0 | <!--limit--> <!--limit_anidb--> | `limit_anidb` | **Description:** Changes the Builder Limit of the AniDB Popular Collection.<br>**Default:** `30`<br>**Values:** Number greater than 0 | <!--limit_anidb--> <!--sort_by--> -| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults File.<br>**Default:** `release.desc`<br>**Values:** [Any `smart_filter` Sort Option](../../files/builders/plex.md#sort-options) | -| `sort_by_<<key>>`<sup>**1**</sup> | **Description:** Changes the Smart Filter Sort of the [key's](#collection_section) collection.<br>**Default:** `sort_by`<br>**Values:** [Any `smart_filter` Sort Option](../../files/builders/plex.md#sort-options) | +| `sort_by` | **Description:** Changes the Smart Filter Sort for all collections in a Defaults File.<br>**Default:** `release.desc`<br>**Values:** [Any `smart_filter` Sort Option](../../files/builders/plex/sort-options.md) | +| `sort_by_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Smart Filter Sort of the [key's](#collection_section) collection.<br>**Default:** `sort_by`<br>**Values:** [Any `smart_filter` Sort Option](../../files/builders/plex/sort-options.md) | <!--sort_by--> -<!--style--> -| `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `bw`<br>**Values:** `bw`, `rainier`, `signature`, `diiivoy`, or `diiivoycolor` | -<!--style--> <!--resolution-style--> | `style` | **Description:** Controls the visual theme of the collections created.<br>**Default:** `default`<br>**Values:** `default` or `standards` | <!--resolution-style--> @@ -313,7 +311,7 @@ <!--color-style--> <!--sync_mode--> | `sync_mode` | **Description:** Changes the Sync Mode for all collections in a Defaults File.<br>**Default:** `sync`<br>**Values:**{% include-markdown "./tables/sync_mode.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | -| `sync_mode_<<key>>`<sup>**1**</sup> | **Description:** Changes the Sync Mode of the [key's](#collection_section) collection.<br>**Default:** `sync_mode`<br>**Values:**{% include-markdown "./tables/sync_mode.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | +| `sync_mode_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the Sync Mode of the [key's](#collection_section) collection.<br>**Default:** `sync_mode`<br>**Values:**{% include-markdown "./tables/sync_mode.md" replace='{"\n": "", "\t": ""}' rewrite-relative-urls=false %} | <!--sync_mode--> <!--format--> | `name_format` | **Description:** Changes the title format of the Dynamic Collections.<br>**Default:** `NAME_FORMAT`<br>**Values:** Any string with `<<key_name>>` in it. | @@ -324,7 +322,7 @@ | `tmdb_deathday` | **Description:** Controls if the Definition is run based on `tmdb_person`'s Deathday. Has 3 possible attributes `this_month`, `before` and `after`.<br>**Values:**<table class="clearTable"><tr><td>`this_month`</td><td>Run's if Deathday is in current Month</td><td>`true`/`false`</td></tr><tr><td>`before`</td><td>Run if X Number of Days before the Deathday</td><td>Number 0 or greater</td></tr><tr><td>`after`</td><td>Run if X Number of Days after the Deathday</td><td>Number 0 or greater</td></tr></table> | <!--tmdb_birthday--> <!--tmdb_person_offset--> -| `tmdb_person_offset_<<key>>`<sup>**1**</sup> | **Description:** Changes the summary tmdb_person_offset for the specific key.<br>**Default:** `0`<br>**Values:** Dictionary of Actor Name as the keys and the tmdb_person_offset as the value. | +| `tmdb_person_offset_<<key>>` :material-numeric-1-circle:{ data-tooltip data-tooltip-id="tippy-defaults-key" } | **Description:** Changes the summary tmdb_person_offset for the specific key.<br>**Default:** `0`<br>**Values:** Dictionary :material-information-outline:{ data-tooltip data-tooltip-id="tippy-yaml-dictionaries" } of Actor Name as the keys and the tmdb_person_offset as the value. | <!--tmdb_person_offset--> <!--sup1--> diff --git a/json-schema/config-schema.json b/json-schema/config-schema.json index 71a7ae981..6433a1995 100644 --- a/json-schema/config-schema.json +++ b/json-schema/config-schema.json @@ -159,30 +159,58 @@ }, "language": { "type": ["string", "null"], - "enum": [ + "enum": ["", "aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", - "ay", "az", "ba", "be", "bg", "bh", "bi", "bm", "bn", "bo", - "br", "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", - "cy", "da", "de", "dz", "ee", "el", "en", "eo", "es", "et", - "eu", "fa", "fi", "fj", "fo", "fr", "fy", "ga", "gd", "gl", - "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", - "ia", "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", - "ja", "jv", "ka", "kg", "ki", "kj", "kk", "kl", "km", "kn", - "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", - "li", "ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", - "ml", "mn", "mo", "mr", "ms", "mt", "nb", "nd", "ne", "nl", - "nn", "no", "oc", "oj", "om", "or", "os", "pa", "pi", "pl", - "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "se", "sg", - "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", - "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", "tl", - "tn", "to", "tr", "ts", "tt", "tw", "ug", "uk", "ur", "uz", - "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh", "zu" + "ay", "az", "ba", "be", "bg", "bi", "bm", "bn", "bo", "br", + "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", "cy", + "da", "de", "dv", "dz", "ee", "el", "en", "eo", "es", "et", + "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", "gd", + "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", + "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii", "ik", "io", + "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", "kk", + "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", + "la", "lb", "lg", "li", "ln", "lo", "lt", "lu", "lv", "mg", + "mh", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "na", + "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", + "oc", "oj", "om", "or", "os", "pa", "pi", "pl", "ps", "pt", + "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", "se", + "sg", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", + "st", "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", + "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", "uk", + "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", + "za", "zh", "zu" ], "description": "This field can be either null or a valid ISO 639 language code." }, "region": { "type": "string", - "enum": ["","AD","AE","AF","AG","AI","AL","AM","AN","AO","AQ","AR","AS","AT","AU","AW","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BM","BN","BO","BR","BS","BT","BU","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CS","CU","CV","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","GB","GD","GE","GF","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IN","IO","IQ","IR","IS","IT","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SU","SV","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TP","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","XC","XG","XI","XK","YE","YT","YU","ZA","ZM","ZR","ZW"], + "enum": ["", + "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", + "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", + "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", + "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", + "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", + "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", + "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", + "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", + "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", + "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", + "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", + "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", + "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", + "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", + "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", + "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", + "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", + "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", + "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", + "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", + "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", + "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", + "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", + "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", + "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" + ], "description": "This field can be either null or a valid ISO 3166-1 Code." }, "cache_expiration": { @@ -344,24 +372,26 @@ }, "language": { "type": ["string", "null"], - "enum": [ + "enum": ["", "aa", "ab", "ae", "af", "ak", "am", "an", "ar", "as", "av", - "ay", "az", "ba", "be", "bg", "bh", "bi", "bm", "bn", "bo", - "br", "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", - "cy", "da", "de", "dz", "ee", "el", "en", "eo", "es", "et", - "eu", "fa", "fi", "fj", "fo", "fr", "fy", "ga", "gd", "gl", - "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", - "ia", "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", - "ja", "jv", "ka", "kg", "ki", "kj", "kk", "kl", "km", "kn", - "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", - "li", "ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", - "ml", "mn", "mo", "mr", "ms", "mt", "nb", "nd", "ne", "nl", - "nn", "no", "oc", "oj", "om", "or", "os", "pa", "pi", "pl", - "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "se", "sg", - "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", - "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", "tl", - "tn", "to", "tr", "ts", "tt", "tw", "ug", "uk", "ur", "uz", - "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh", "zu" + "ay", "az", "ba", "be", "bg", "bi", "bm", "bn", "bo", "br", + "bs", "ca", "ce", "ch", "co", "cr", "cs", "cu", "cv", "cy", + "da", "de", "dv", "dz", "ee", "el", "en", "eo", "es", "et", + "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", "gd", + "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", + "hu", "hy", "hz", "ia", "id", "ie", "ig", "ii", "ik", "io", + "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", "kk", + "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", + "la", "lb", "lg", "li", "ln", "lo", "lt", "lu", "lv", "mg", + "mh", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "na", + "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", + "oc", "oj", "om", "or", "os", "pa", "pi", "pl", "ps", "pt", + "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", "se", + "sg", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", + "st", "su", "sv", "sw", "ta", "te", "tg", "th", "ti", "tk", + "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", "uk", + "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", + "za", "zh", "zu" ], "description": "This field can be either null or a valid ISO 639 language code." }, @@ -421,7 +451,7 @@ }, "monitor": { "type": "string", - "enum": ["all","future","missing","existing","pilot","first","latest","none"] + "enum": ["all", "future", "missing", "existing", "pilot", "first", "latest", "none"] }, "monitor_existing": { "description": "Ensures all existing shows in collections match your monitor setting.\nUse the sonarr_monitor_existing Sonarr Setting in the collection definition to match the monitor setting per collection.", @@ -435,7 +465,7 @@ }, "series_type": { "type": "string", - "enum": ["standard","daily","anime"] + "enum": ["standard", "daily", "anime"] }, "tag": { "type": [ "string", "null" ] @@ -496,7 +526,7 @@ }, "monitor": { "type": "string", - "enum": ["all","future","missing","existing","pilot","first","latest","none"] + "enum": ["all", "future", "missing", "existing", "pilot", "first", "latest", "none"] }, "monitor_existing": { "description": "Ensures all existing shows in collections match your monitor setting.\nUse the sonarr_monitor_existing Sonarr Setting in the collection definition to match the monitor setting per collection.", @@ -510,7 +540,7 @@ }, "series_type": { "type": "string", - "enum": ["standard","daily","anime"] + "enum": ["standard", "daily", "anime"] }, "tag": { "type": [ "string", "null" ] @@ -569,7 +599,7 @@ }, "availability": { "type": "string", - "enum": ["announced","cinemas","released","db"] + "enum": ["announced", "cinemas", "released", "db"] }, "tag": { "type": [ "string", "null" ] @@ -633,7 +663,7 @@ }, "availability": { "type": "string", - "enum": ["announced","cinemas","released","db"] + "enum": ["announced", "cinemas", "released", "db"] }, "tag": { "type": [ "string", "null" ] @@ -792,26 +822,26 @@ }, "tvdb_language": { "type": ["string", "null"], - "enum": [ + "enum": ["", "aar", "abk", "afr", "aka", "alb", "amh", "ara", "arg", "arm", "asm", - "ava", "ave", "aym", "aze", "bak", "bam", "bel", "ben", "bih", "bis", - "bod", "bos", "bre", "bul", "cat", "cha", "che", "chi", "chu", "chv", - "cor", "cos", "cre", "ces", "dan", "deu", "div", "dzo", "ell", "eng", - "epo", "est", "eus", "ewe", "fao", "fas", "fij", "fin", "fra", "fry", - "ful", "gla", "gle", "glg", "glv", "grn", "guj", "hat", "hau", "heb", - "her", "hin", "hmo", "hrv", "hun", "hye", "ibo", "ido", "iii", "iku", - "ile", "ina", "ind", "ipk", "isl", "ita", "jav", "jpn", "kal", "kan", - "kas", "kat", "kau", "kaz", "khm", "kik", "kin", "kir", "kom", "kon", - "kor", "kua", "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", - "lub", "lug", "mah", "mal", "mar", "mkd", "mlg", "mlt", "mon", "mri", - "msa", "mya", "nau", "nav", "nbl", "nde", "ndo", "nep", "nld", "nno", - "nob", "nor", "nya", "oci", "oji", "ori", "orm", "oss", "pan", "pli", - "pol", "por", "pus", "que", "roh", "ron", "run", "rus", "sag", "san", - "sin", "slk", "slv", "sme", "smo", "sna", "snd", "som", "sot", "spa", - "srd", "srp", "ssw", "sun", "swa", "swe", "tah", "tam", "tat", "tel", - "tgk", "tgl", "tha", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", - "uig", "ukr", "urd", "uzb", "ven", "vie", "vol", "wln", "wol", "xho", - "yid", "yor", "zha", "zho", "zul" + "ava", "ave", "aym", "aze", "bak", "bam", "baq", "bel", "ben", "bis", + "bos", "bre", "bul", "bur", "cat", "cha", "che", "chi", "chu", "chv", + "cor", "cos", "cre", "cze", "dan", "div", "dut", "dzo", "eng", "epo", + "est", "ewe", "fao", "fij", "fin", "fre", "fry", "ful", "geo", "ger", + "gla", "gle", "glg", "glv", "gre", "grn", "guj", "hat", "hau", "heb", + "her", "hin", "hmo", "hrv", "hun", "ibo", "ice", "ido", "iii", "iku", + "ile", "ina", "ind", "ipk", "ita", "jav", "jpn", "kal", "kan", "kas", + "kau", "kaz", "khm", "kik", "kin", "kir", "kom", "kon", "kor", "kua", + "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", "lub", "lug", + "mac", "mah", "mal", "mao", "mar", "may", "mlg", "mlt", "mon", "nau", + "nav", "nbl", "nde", "ndo", "nep", "nno", "nob", "nor", "nya", "oci", + "oji", "ori", "orm", "oss", "pan", "per", "pli", "pol", "por", "pus", + "que", "roh", "rum", "run", "rus", "sag", "san", "sin", "slo", "slv", + "sme", "smo", "sna", "snd", "som", "sot", "spa", "srd", "srp", "ssw", + "sun", "swa", "swe", "tah", "tam", "tat", "tel", "tgk", "tgl", "tha", + "tib", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", "uig", "ukr", + "urd", "uzb", "ven", "vie", "vol", "wel", "wln", "wol", "xho", "yid", + "yor", "zha", "zul" ], "description": "Specify the language to query TVDb in.\nThis field can be either null or a valid ISO 639-2 language code." }, @@ -981,7 +1011,7 @@ "properties": { "pmm": { "type": "string", - "enum": ["actor", "anilist","aspect","audio_language","bafta","based","basic","berlinale","cannes","cesar","choice","collectionless","content_rating_au","content_rating_cs","content_rating_de","content_rating_nz","content_rating_mal","content_rating_uk","content_rating_us","continent","country","decade","director","emmy","flixpatrol","franchise","genre","golden","imdb","letterboxd","myanimelist","network","nfr","oscars","other_chart","pca","producer","razzie","region","resolution","sag","seasonal","separator_award","separator_chart","spirit","streaming","studio","subtitle_language","sundance","tautulli","tiff","tmdb","trakt","universe","venice","writer","year"] + "enum": ["actor", "anilist", "aspect", "audio_language", "bafta", "based", "basic", "berlinale", "cannes", "cesar", "choice", "collectionless", "composer","content_rating_au", "content_rating_cs", "content_rating_de", "content_rating_nz", "content_rating_mal", "content_rating_uk", "content_rating_us", "continent", "country", "decade", "director", "emmy", "flixpatrol", "franchise", "genre", "golden", "imdb", "letterboxd", "myanimelist", "network", "nfr", "oscars", "other_chart", "pca", "producer", "razzie", "region", "resolution", "sag", "seasonal", "separator_award", "separator_chart", "spirit", "streaming", "studio", "subtitle_language", "sundance", "tautulli", "tiff", "tmdb", "trakt", "universe", "venice", "writer", "year"] }, "schedule": { "type": "string" @@ -1006,7 +1036,7 @@ "properties": { "default": { "type": "string", - "enum": ["actor", "anilist","aspect","audio_language","bafta","based","basic","berlinale","cannes","cesar","choice","collectionless","content_rating_au","content_rating_cs","content_rating_de","content_rating_nz","content_rating_mal","content_rating_uk","content_rating_us","continent","country","decade","director","emmy","flixpatrol","franchise","genre","golden","imdb","letterboxd","myanimelist","network","nfr","oscars","other_chart","pca","producer","razzie","region","resolution","sag","seasonal","separator_award","separator_chart","spirit","streaming","studio","subtitle_language","sundance","tautulli","tiff","tmdb","trakt","universe","venice","writer","year"] + "enum": ["actor", "anilist", "aspect", "audio_language", "bafta", "based", "basic", "berlinale", "cannes", "cesar", "choice", "collectionless", "composer","content_rating_au", "content_rating_cs", "content_rating_de", "content_rating_nz", "content_rating_mal", "content_rating_uk", "content_rating_us", "continent", "country", "decade", "director", "emmy", "flixpatrol", "franchise", "genre", "golden", "imdb", "letterboxd", "myanimelist", "network", "nfr", "oscars", "other_chart", "pca", "producer", "razzie", "region", "resolution", "sag", "seasonal", "separator_award", "separator_chart", "spirit", "streaming", "studio", "subtitle_language", "sundance", "tautulli", "tiff", "tmdb", "trakt", "universe", "venice", "writer", "year"] }, "schedule": { "type": "string" @@ -1031,7 +1061,7 @@ "properties": { "pmm": { "type": "string", - "enum": ["aspect","audio_codec","commonsense","content_rating_au","content_rating_de","content_rating_nz","content_rating_uk","content_rating_us_movie","content_rating_us_show","direct_play","episode_info","language_count","flixpatrol","languages","mediastinger","network","ratings","resolution","ribbon","runtimes","status","streaming","studio","versions","video_format"] + "enum": ["aspect", "audio_codec", "commonsense", "content_rating_au", "content_rating_de", "content_rating_nz", "content_rating_uk", "content_rating_us_movie", "content_rating_us_show", "direct_play", "episode_info", "language_count", "flixpatrol", "languages", "mediastinger", "network", "ratings", "resolution", "ribbon", "runtimes", "status", "streaming", "studio", "versions", "video_format"] }, "remove_overlays": { "type": "boolean" @@ -1044,7 +1074,7 @@ }, "reset_overlays": { "type": "string", - "enum": ["tmdb","plex"] + "enum": ["tmdb", "plex"] }, "schedule": { "type": "string" @@ -1069,7 +1099,7 @@ "properties": { "default": { "type": "string", - "enum": ["aspect","audio_codec","commonsense","content_rating_au","content_rating_de","content_rating_nz","content_rating_uk","content_rating_us_movie","content_rating_us_show","direct_play","episode_info","language_count","flixpatrol","languages","mediastinger","network","ratings","resolution","ribbon","runtimes","status","streaming","studio","versions","video_format"] + "enum": ["aspect", "audio_codec", "commonsense", "content_rating_au", "content_rating_de", "content_rating_nz", "content_rating_uk", "content_rating_us_movie", "content_rating_us_show", "direct_play", "episode_info", "language_count", "flixpatrol", "languages", "mediastinger", "network", "ratings", "resolution", "ribbon", "runtimes", "status", "streaming", "studio", "versions", "video_format"] }, "remove_overlays": { "type": "boolean" @@ -1082,7 +1112,7 @@ }, "reset_overlays": { "type": "string", - "enum": ["tmdb","plex"] + "enum": ["tmdb", "plex"] }, "schedule": { "type": "string" @@ -1624,11 +1654,11 @@ "reset_overlays": { "description": "Used to reset overlays from this library only. This will reset overlays to every item in your library to your source choice. This will use the reset image when overlaying items in your library.\nThis will reset all posters to the desired source on each run and will reapply all overlays on each run, which will result in image bloat.", "type": "string", - "enum": ["tmdb","plex"] + "enum": ["tmdb", "plex"] }, "run_order": { "type": "array", "uniqueItems": true, "items": {"type": "string", - "enum": ["collections","metadata","operations","overlays"] } + "enum": ["collections", "metadata", "operations", "overlays"] } } } }, @@ -1857,6 +1887,9 @@ "assets_for_all": { "type": "boolean" }, + "assets_for_all_collections": { + "type": "boolean" + }, "update_blank_track_titles": { "type": "boolean" }, @@ -1895,7 +1928,7 @@ "type": "string", "anyOf": [ { - "enum": ["mdb","mdb_commonsense","mdb_commonsense0","omdb","mal","lock","unlock","remove","reset"]}, + "enum": ["mdb", "mdb_commonsense", "mdb_commonsense0", "omdb", "mal", "lock", "unlock", "remove", "reset"]}, { "pattern": ".*" } @@ -1908,7 +1941,7 @@ "type": "string", "anyOf": [ { - "enum": ["anidb", "anidb_official", "mal","mal_english", "mal_japanese","lock", "unlock", "remove", "reset"] + "enum": ["anidb", "anidb_official", "mal", "mal_english", "mal_japanese", "lock", "unlock", "remove", "reset"] }, { "pattern": ".*" @@ -1918,52 +1951,91 @@ }, "mass_studio_update": { "anyOf": [ - { - "type": "string", - "enum": ["anidb","mal","tmdb","lock","unlock","remove","reset"] - }, - { - "type": "array", - "uniqueItems": true, - "items": + { + "type": "string", + "enum": ["anidb", "mal", "tmdb", "lock", "unlock", "remove", "reset"] + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ { - "anyOf": [ - {"enum": ["anidb","mal","tmdb","lock","unlock","remove","reset"]} + "type": "string", + "enum": ["anidb", "mal", "tmdb", "lock", "unlock", "remove", "reset"] + }, + { + "type": "string" + } ] } } ] }, "mass_originally_available_update": { + "anyOf": [ + { + "type": "string", + "enum": ["tmdb", "tvdb", "omdb", "mdb", "mdb_digital", "anidb", "mal", "lock", "unlock", "remove", "reset"] + }, + { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ + { + "type": "string", + "enum": ["tmdb", "tvdb", "omdb", "mdb", "mdb_digital", "anidb", "mal", "lock", "unlock", "remove", "reset"] + }, + { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + } + ] + } + } + ] + }, + "mass_added_at_update": { "anyOf": [ { "type": "string", - "enum": ["tmdb","tvdb","omdb","mdb","anidb","mal","lock","unlock","remove","reset"] + "enum": ["tmdb", "tvdb", "omdb", "mdb", "mdb_digital", "anidb", "mal", "lock", "unlock", "remove", "reset"] }, { - "type": "array", - "uniqueItems": true, - "items": + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + { + "type": "array", + "uniqueItems": true, + "items": { + "anyOf": [ { - "anyOf": [ - {"enum": ["tmdb","tvdb","omdb","mdb","anidb","mal","lock","unlock","remove","reset"]} - ] + "type": "string", + "enum": ["tmdb", "tvdb", "omdb", "mdb", "mdb_digital", "anidb", "mal", "lock", "unlock", "remove", "reset"] + }, + { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } - } - ] - }, - "mass_added_at_update": { - "type": "string", - "enum": ["tmdb","tvdb","omdb","mdb","anidb","mal","lock","unlock","remove","reset"] - }, - "mass_audience_rating_update": { + ] + } + } + ] + }, + "mass_audience_rating_update": { "anyOf": [ { "type": "number" }, { "type": "string", - "enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -1971,7 +2043,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -1985,7 +2057,7 @@ }, { "type": "string", - "enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -1993,7 +2065,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -2007,7 +2079,7 @@ }, { "type": "string", - "enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -2015,7 +2087,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","trakt_user","omdb","mdb","mdb_average","mdb_imdb","mdb_metacritic","mdb_metacriticuser","mdb_trakt","mdb_tomatoes","mdb_tomatoesaudience","mdb_tmdb","mdb_letterboxd","mdb_myanimelist","anidb_rating","anidb_average","anidb_score","mal","lock","unlock","remove","reset"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -2029,7 +2101,7 @@ }, { "type": "string", - "enum": ["tmdb","imdb","lock","remove","reset","unlock"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -2037,7 +2109,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","lock","remove","reset","unlock"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -2051,7 +2123,7 @@ }, { "type": "string", - "enum": ["tmdb","imdb","lock","remove","reset","unlock"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -2059,7 +2131,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","lock","remove","reset","unlock"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -2073,7 +2145,7 @@ }, { "type": "string", - "enum": ["tmdb","imdb","lock","remove","reset","unlock"] + "enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"] }, { "type": "array", @@ -2081,7 +2153,7 @@ "items": { "anyOf": [ - {"enum": ["tmdb","imdb","lock","remove","reset","unlock"]}, + {"enum": ["anidb_average", "anidb_rating", "anidb_score", "imdb", "lock", "mal", "mdb", "mdb_average", "mdb_imdb", "mdb_letterboxd", "mdb_metacritic", "mdb_metacriticuser", "mdb_myanimelist", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_trakt", "omdb", "omdb_metascore", "omdb_tomatoes", "plex_imdb", "plex_tmdb", "plex_tomatoes", "plex_tomatoesaudience", "remove", "reset", "tmdb", "trakt", "trakt_user", "unlock"]}, {"type": "number"} ] } @@ -2089,23 +2161,66 @@ ] }, "mass_poster_update": { - "type": "string", - "enum": ["tmdb","plex","lock","unlock"] + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["tmdb", "plex", "lock", "unlock"] + }, + "seasons": { + "type": "boolean" + }, + "episodes": { + "type": "boolean" + }, + "ignore_locked": { + "type": "boolean" + }, + "ignore_overlays": { + "type": "boolean" + } + }, + "additionalProperties": false }, "mass_background_update": { - "type": "string", - "enum": ["tmdb","plex","lock","unlock"] + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["tmdb", "plex", "lock", "unlock"] + }, + "seasons": { + "type": "boolean" + }, + "episodes": { + "type": "boolean" + }, + "ignore_locked": { + "type": "boolean" + } + }, + "additionalProperties": false }, "mass_imdb_parental_labels": { "type": "string", - "enum": ["none","mild","moderate","severe"] + "enum": ["none", "mild", "moderate", "severe"] }, "mass_collection_mode": { "type": "string", - "enum": ["default","hide","hide_items","show_items"] + "enum": ["default", "hide", "hide_items", "show_items"] + }, + "radarr_remove_by_tag": { + "type": "array", + "items": { + "type": "string" + } + }, + "sonarr_remove_by_tag": { + "type": "array", + "items": { + "type": "string" + } }, - "radarr_remove_by_tag": { "type": "string" }, - "sonarr_remove_by_tag": { "type": "string" }, "delete_collections": { "type": "object", "properties": { @@ -2217,8 +2332,8 @@ "additionalProperties": false, "properties": { "addon_offset": { "type": "integer", "exclusiveMinimum": 0 }, - "addon_position": { "type": "string", "enum": ["left","top","bottom","right"] }, - "back_align": { "type": "string", "enum": ["left","right","center","top","bottom"] }, + "addon_position": { "type": "string", "enum": ["left", "top", "bottom", "right"] }, + "back_align": { "type": "string", "enum": ["left", "right", "center", "top", "bottom"] }, "back_color": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "back_height": { "type": "integer", "exclusiveMinimum": 0 }, "back_line_color": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, @@ -2226,37 +2341,41 @@ "back_padding": { "type": "integer", "exclusiveMinimum": 0 }, "back_radius": { "type": "integer", "exclusiveMinimum": 0 }, "back_width": { "type": "integer", "exclusiveMinimum": 0 }, - "builder_level": { "type": "string", "enum": ["show","season","episode"] }, + "builder_level": { "type": "string", "enum": ["show", "season", "episode"] }, + "color": { + "type": "boolean", + "description": "Toggles color styling for applicable overlays like content ratings." + }, "file": { "type": "string" }, "final_name": { "type": "string" }, - "flag_alignment": { "type": "string", "enum": ["left","right"] }, + "flag_alignment": { "type": "string", "enum": ["left", "right"] }, "font_color": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "font_size": { "type": "integer", "exclusiveMinimum": 0 }, - "font_style": { "type": "string", "enum": ["Any","Italic","Normal","Oblique"] }, + "font_style": { "type": "string", "enum": ["Any", "Italic", "Normal", "Oblique"] }, "font": { "type": "string" }, "git": { "type": "string" }, - "group_alignment": { "type": "string", "enum": ["horizontal","vertical"] }, + "group_alignment": { "type": "string", "enum": ["horizontal", "vertical"] }, "hide_text": { "type": "boolean" }, - "horizontal_align": { "type": "string", "enum": ["left","center","right"] }, + "horizontal_align": { "type": "string", "enum": ["left", "center", "right"] }, "horizontal_offset": { "type": ["string", "integer"] }, - "horizontal_position": { "type": "string", "enum": ["left","left2","center","center_left","center_right","right","right2"] }, + "horizontal_position": { "type": "string", "enum": ["left", "left2", "center", "center_left", "center_right", "right", "right2"] }, "horizontal_spacing": { "type": "integer" }, - "initial_horizontal_align": { "type": "string", "enum": ["left","center","right"] }, + "initial_horizontal_align": { "type": "string", "enum": ["left", "center", "right"] }, "initial_horizontal_offset": { "type": "integer" }, - "initial_vertical_align": { "type": "string", "enum": ["top","center","bottom"] }, + "initial_vertical_align": { "type": "string", "enum": ["top", "center", "bottom"] }, "initial_vertical_offset": { "type": "integer" }, "languages": { "type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[a-z]{2}$" }}, "last": { "type": "integer", "exclusiveMinimum": 0 }, "limit": { "type": "integer", "exclusiveMinimum": 0 }, - "location": { "type": "string", "enum": ["world","albania","argentina","armenia","australia","austria","azerbaijan","bahamas","bahrain","bangladesh","belarus","belgium","belize","benin","bolivia","bosnia_and_herzegovina","botswana","brazil","bulgaria","burkina_faso","cambodia","canada","chile","colombia","costa_rica","croatia","cyprus","czech_republic","denmark","dominican_republic","ecuador","egypt","estonia","finland","france","gabon","germany","ghana","greece","guatemala","guinea_bissau","haiti","honduras","hong_kong","hungary","iceland","india","indonesia","ireland","israel","italy","ivory_coast","jamaica","japan","jordan","kazakhstan","kenya","kuwait","kyrgyzstan","laos","latvia","lebanon","lithuania","luxembourg","malaysia","maldives","mali","malta","mexico","moldova","mongolia","montenegro","morocco","mozambique","namibia","netherlands","new_zealand","nicaragua","niger","nigeria","north_macedonia","norway","oman","pakistan","panama","papua_new_guinea","paraguay","peru","philippines","poland","portugal","qatar","romania","russia","rwanda","salvador","saudi_arabia","senegal","serbia","singapore","slovakia","slovenia","south_africa","south_korea","spain","sri_lanka","sweden","switzerland","taiwan","tajikistan","tanzania","thailand","togo","trinidad_and_tobago","turkey","turkmenistan","uganda","ukraine","united_arab_emirates","united_kingdom","united_states","uruguay","uzbekistan","venezuela","vietnam","zambia","zimbabwe"] }, + "location": { "type": "string", "enum": ["world", "albania", "argentina", "armenia", "australia", "austria", "azerbaijan", "bahamas", "bahrain", "bangladesh", "belarus", "belgium", "belize", "benin", "bolivia", "bosnia_and_herzegovina", "botswana", "brazil", "bulgaria", "burkina_faso", "cambodia", "canada", "chile", "colombia", "costa_rica", "croatia", "cyprus", "czech_republic", "denmark", "dominican_republic", "ecuador", "egypt", "estonia", "finland", "france", "gabon", "germany", "ghana", "greece", "guatemala", "guinea_bissau", "haiti", "honduras", "hong_kong", "hungary", "iceland", "india", "indonesia", "ireland", "israel", "italy", "ivory_coast", "jamaica", "japan", "jordan", "kazakhstan", "kenya", "kuwait", "kyrgyzstan", "laos", "latvia", "lebanon", "lithuania", "luxembourg", "malaysia", "maldives", "mali", "malta", "mexico", "moldova", "mongolia", "montenegro", "morocco", "mozambique", "namibia", "netherlands", "new_zealand", "nicaragua", "niger", "nigeria", "north_macedonia", "norway", "oman", "pakistan", "panama", "papua_new_guinea", "paraguay", "peru", "philippines", "poland", "portugal", "qatar", "romania", "russia", "rwanda", "salvador", "saudi_arabia", "senegal", "serbia", "singapore", "slovakia", "slovenia", "south_africa", "south_korea", "spain", "sri_lanka", "sweden", "switzerland", "taiwan", "tajikistan", "tanzania", "thailand", "togo", "trinidad_and_tobago", "turkey", "turkmenistan", "uganda", "ukraine", "united_arab_emirates", "united_kingdom", "united_states", "uruguay", "uzbekistan", "venezuela", "vietnam", "zambia", "zimbabwe"] }, "minimum": { "type": "integer"}, "offset": { "type": "integer", "minimum": 0 }, "originals_only": { "description": "Changes Streaming Service overlays to only apply to original content produced by the service.\nNote: Cannot be used with region, and only produces overlays for amazon, appletv, disney, max, hulu, netflix, paramount, peacock", "type": "boolean" }, "overlay_limit": { "description": "Choose the number of overlay this queue displays.\nDefault: 3\nValues: 1, 2, 3, 4, or 5", "type": "integer", "minimum": 1, "maximum": 5 }, - "position": { "type": "string", "enum": ["left","right"] }, + "position": { "type": "string", "enum": ["left", "right"] }, "post_nr_text": { "description": "Choose the text after the 'nr' key for the Overlay.\nValues: Any String", "type": "string" }, "post_text": { "description": "Choose the text after the key for the Overlay.\nDefault: +\nValues: Any String", "type": "string" }, - "pre_nr_text": { "description": "Choose the text before the 'nr' key for the Overlay.\nValues: Any String","type": "string" }, + "pre_nr_text": { "description": "Choose the text before the 'nr' key for the Overlay.\nValues: Any String", "type": "string" }, "pre_text": { "description": "Choose the text before the key for the Overlay.\nValues: Any String", "type": "string" }, "rating_alignment": { "type": "string", "enum": ["vertical", "horizontal"] }, "rating1": { "type": "string", "enum": ["critic", "audience", "user"] }, @@ -2281,17 +2400,17 @@ "size": { "type": "string", "enum": ["small", "big"] }, "stroke_color": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "stroke_width": { "type": "integer", "exclusiveMinimum": 0 }, - "style": { "type": "string", "enum": ["compact","standard","bigger","round","square","half", "red", "black", "yellow", "gray", "color", "white"] }, + "style": { "type": "string", "enum": ["compact", "standard", "bigger", "round", "square", "half", "red", "black", "yellow", "gray", "color", "white"] }, "text": { "type": "string" }, - "time_window": { "type": "string", "enum": ["today","yesterday","this_week","last_week","this_month","last_month","this_year","last_year"] }, + "time_window": { "type": "string", "enum": ["today", "yesterday", "this_week", "last_week", "this_month", "last_month", "this_year", "last_year"] }, "url": { "type": "string", "format": "uri", "pattern": "^(https?)://" }, "use_edition": { "type": "boolean" }, "use_lowercase": { "type": "boolean" }, "use_resolution": { "type": "boolean" }, "use_subtitles": { "type": "boolean" }, - "vertical_align": { "type": "string", "enum": ["top","center","bottom"] }, + "vertical_align": { "type": "string", "enum": ["top", "center", "bottom"] }, "vertical_offset": { "type": ["string", "integer"] }, - "vertical_position": { "type": "string", "enum": ["top","top2","top3","center","center_top","center_bottom","bottom","bottom2","bottom3"] }, + "vertical_position": { "type": "string", "enum": ["top", "top2", "top3", "center", "center_top", "center_bottom", "bottom", "bottom2", "bottom3"] }, "vertical_spacing": { "type": "integer" } }, "patternProperties": { @@ -2299,26 +2418,26 @@ "^file_.*$": { "type": "string" }, "^git_.*$": { "type": "string" }, "^limit_.*$": { "type": "integer", "exclusiveMinimum": 0 }, - "^location_.*$": { "type": "string", "enum": ["world","albania","argentina","armenia","australia","austria","azerbaijan","bahamas","bahrain","bangladesh","belarus","belgium","belize","benin","bolivia","bosnia_and_herzegovina","botswana","brazil","bulgaria","burkina_faso","cambodia","canada","chile","colombia","costa_rica","croatia","cyprus","czech_republic","denmark","dominican_republic","ecuador","egypt","estonia","finland","france","gabon","germany","ghana","greece","guatemala","guinea_bissau","haiti","honduras","hong_kong","hungary","iceland","india","indonesia","ireland","israel","italy","ivory_coast","jamaica","japan","jordan","kazakhstan","kenya","kuwait","kyrgyzstan","laos","latvia","lebanon","lithuania","luxembourg","malaysia","maldives","mali","malta","mexico","moldova","mongolia","montenegro","morocco","mozambique","namibia","netherlands","new_zealand","nicaragua","niger","nigeria","north_macedonia","norway","oman","pakistan","panama","papua_new_guinea","paraguay","peru","philippines","poland","portugal","qatar","romania","russia","rwanda","salvador","saudi_arabia","senegal","serbia","singapore","slovakia","slovenia","south_africa","south_korea","spain","sri_lanka","sweden","switzerland","taiwan","tajikistan","tanzania","thailand","togo","trinidad_and_tobago","turkey","turkmenistan","uganda","ukraine","united_arab_emirates","united_kingdom","united_states","uruguay","uzbekistan","venezuela","vietnam","zambia","zimbabwe"] }, + "^location_.*$": { "type": "string", "enum": ["world", "albania", "argentina", "armenia", "australia", "austria", "azerbaijan", "bahamas", "bahrain", "bangladesh", "belarus", "belgium", "belize", "benin", "bolivia", "bosnia_and_herzegovina", "botswana", "brazil", "bulgaria", "burkina_faso", "cambodia", "canada", "chile", "colombia", "costa_rica", "croatia", "cyprus", "czech_republic", "denmark", "dominican_republic", "ecuador", "egypt", "estonia", "finland", "france", "gabon", "germany", "ghana", "greece", "guatemala", "guinea_bissau", "haiti", "honduras", "hong_kong", "hungary", "iceland", "india", "indonesia", "ireland", "israel", "italy", "ivory_coast", "jamaica", "japan", "jordan", "kazakhstan", "kenya", "kuwait", "kyrgyzstan", "laos", "latvia", "lebanon", "lithuania", "luxembourg", "malaysia", "maldives", "mali", "malta", "mexico", "moldova", "mongolia", "montenegro", "morocco", "mozambique", "namibia", "netherlands", "new_zealand", "nicaragua", "niger", "nigeria", "north_macedonia", "norway", "oman", "pakistan", "panama", "papua_new_guinea", "paraguay", "peru", "philippines", "poland", "portugal", "qatar", "romania", "russia", "rwanda", "salvador", "saudi_arabia", "senegal", "serbia", "singapore", "slovakia", "slovenia", "south_africa", "south_korea", "spain", "sri_lanka", "sweden", "switzerland", "taiwan", "tajikistan", "tanzania", "thailand", "togo", "trinidad_and_tobago", "turkey", "turkmenistan", "uganda", "ukraine", "united_arab_emirates", "united_kingdom", "united_states", "uruguay", "uzbekistan", "venezuela", "vietnam", "zambia", "zimbabwe"] }, "^rating\\d+_.*$": { "type": "string" }, "^regex_.*$": { "type": "string" }, "^repo_.*$": { "type": "string" }, "^text_.*$": { "type": "string" }, - "^time_window_.*$": { "type": "string", "enum": ["today","yesterday","this_week","last_week","this_month","last_month","this_year","last_year"] }, + "^time_window_.*$": { "type": "string", "enum": ["today", "yesterday", "this_week", "last_week", "this_month", "last_month", "this_year", "last_year"] }, "^url_.*$": { "type": "string", "format": "uri", "pattern": "^(https?)://" }, "^use_.*$": { "type": "boolean" }, "^weight_.*$": { "type": "integer" }, "^font_.*$": { "type": "string" }, "^font_color_.*$": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "^font_size_.*$": { "type": "integer", "exclusiveMinimum": 0 }, - "^font_style_.*$": { "type": "string", "enum": ["Any","Italic","Normal","Oblique"] }, - "^horizontal_align_.*$": { "type": "string", "enum": ["left","center","right"] }, + "^font_style_.*$": { "type": "string", "enum": ["Any", "Italic", "Normal", "Oblique"] }, + "^horizontal_align_.*$": { "type": "string", "enum": ["left", "center", "right"] }, "^horizontal_offset_.*$": { "type": ["string", "integer"], "minimum": 0 }, - "^vertical_align_.*$": { "type": "string", "enum": ["top","center","bottom"] }, + "^vertical_align_.*$": { "type": "string", "enum": ["top", "center", "bottom"] }, "^vertical_offset_.*$": { "type": ["string", "integer"], "minimum": 0 }, "^stroke_color_.*$": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "^stroke_width_.*$": { "type": "integer", "exclusiveMinimum": 0 }, - "^back_align_.*$": { "type": "string", "enum": ["left","right","center","top","bottom"] }, + "^back_align_.*$": { "type": "string", "enum": ["left", "right", "center", "top", "bottom"] }, "^back_color_.*$": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, "^back_height_.*$": { "type": "integer", "exclusiveMinimum": 0 }, "^back_line_color_.*$": { "type": "string", "pattern": "^\\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$" }, @@ -2345,4 +2464,4 @@ "title": "template_variables" } } -} \ No newline at end of file +} diff --git a/json-schema/kitchen_sink_config.yml b/json-schema/kitchen_sink_config.yml index 299dc8a29..f10c628a1 100644 --- a/json-schema/kitchen_sink_config.yml +++ b/json-schema/kitchen_sink_config.yml @@ -145,6 +145,15 @@ libraries: data: depth: 1 limit: 15 + - default: composer # Composers + template_variables: # bw, rainier, diiivoy, diiivoycolor, or orig style is used. depth and limit is set low, but sometimes I boost to 10, 150 + exclude: # ever have some random person... you can exclude them if you want +# - Jeremy Kleiner +# - Thomas Hayslip + style: signature + data: + depth: 1 + limit: 15 - default: writer # Writers template_variables: # bw, rainier, diiivoy, diiivoycolor, or orig style is used. depth and limit is set low, but sometimes I boost to 10, 150 exclude: # ever have some random person... you can exclude them if you want diff --git a/json-schema/prototype_config.yml b/json-schema/prototype_config.yml index 226fc1511..b3a69a0ca 100644 --- a/json-schema/prototype_config.yml +++ b/json-schema/prototype_config.yml @@ -53,6 +53,7 @@ libraries: - default: actor - default: director - default: producer + - default: composer - default: writer - default: year - default: decade @@ -201,6 +202,7 @@ libraries: - default: actor - default: director - default: producer + - default: composer - default: writer - default: year - default: decade diff --git a/kometa.py b/kometa.py index d1af33c4c..2d697c55e 100644 --- a/kometa.py +++ b/kometa.py @@ -794,7 +794,59 @@ def run_libraries(config): def run_collection(config, library, metadata, requested_collections): logger.info("") - for mapping_name, collection_attrs in requested_collections.items(): + + all_items = requested_collections.items() + new_collections = {} + + for key, value in requested_collections.items(): + try: + templates = value['template'] + except: + raise Failed(f"Error getting template from {value}") + has_allowed_libraries = any('allowed_libraries' in template for template in templates) + + # import copy + # new_key = f"{library.name} {key}" + # new_value = copy.deepcopy(value) + # for template in new_value['template']: + # # Update 'translation_key' + # if 'translation_key' in template: + # template['translation_key'] = f"{library.name} {template['translation_key']}" + + new_collections[key] = value + + + # if has_allowed_libraries: + # Keep the item as is + # new_collections[new_key] = value + # For entries without 'allowed_libraries', create new entries + # new_key = f"{key} {library.name}" + # new_value = copy.deepcopy(value) + # for lib_type in [library.name]: + # Deep copy the value to modify + # Update 'variables' key + # new_value['variables']['key'] = f"{value['variables']['key']}_{lib_type}s" + # Update 'allowed_libraries' in 'shared' template + # Find the 'shared' template dict + # new_value['postfix'] = f" {'Movies' if lib_type == 'movie' else 'Shows'}" + # for template in new_value['template']: + # if template['name'] == 'shared': + # template['allowed_libraries'] = lib_type + # Update 'translation_key' + # if 'translation_key' in template: + # template['translation_key'] = f"{template['translation_key']}_{lib_type}s" + # Add the new item to new_collections + + # Now, when iterating over new_collections, use .items() + # for mapping_name, collection_attrs in new_collections.items(): + # print(f"Mapping Name: {mapping_name}") + # print(f"Collection Attributes: {collection_attrs}") + # print("-" * 40) + # print(all_items) + + # print(new_collections) + + for mapping_name, collection_attrs in new_collections.items(): collection_start = datetime.now() if run_args["tests"] and ("test" not in collection_attrs or collection_attrs["test"] is not True): no_template_test = True @@ -862,8 +914,12 @@ def run_collection(config, library, metadata, requested_collections): raise Failed(e) builder.display_filters() + # print(f"Founditems: {builder.found_items}") - if len(builder.found_items) > 0 and len(builder.found_items) + builder.beginning_count >= builder.minimum and builder.build_collection: + beginning_count = builder.beginning_count if builder.beginning_count is not None else 0 + if len(builder.found_items) > 0 and len( + builder.found_items) + beginning_count >= builder.minimum and builder.build_collection: + xtest= 1 items_added, items_unchanged = builder.add_to_collection() library.stats["added"] += items_added library.status[str(mapping_name)]["added"] = items_added @@ -886,7 +942,9 @@ def run_collection(config, library, metadata, requested_collections): raise NonExisting(f"{builder.Type} Warning: No items found") valid = True - if builder.build_collection and not builder.blank_collection and items_added + builder.beginning_count < builder.minimum: + # builder.beginning_count = 0 + # if builder.build_collection and not builder.blank_collection and items_added + builder.beginning_count < builder.minimum: + if builder.build_collection and not builder.blank_collection and items_added + builder.beginning_count - items_removed < builder.minimum: logger.info("") logger.info(f"{builder.Type} Minimum: {builder.minimum} not met for {mapping_name} Collection") delete_status = f"Minimum {builder.minimum} Not Met" @@ -913,13 +971,19 @@ def run_collection(config, library, metadata, requested_collections): run_item_details = False logger.info("") logger.separator(f"No {builder.Type} to Update", space=False, border=False) - else: - details_list = builder.update_details() + # else: + # Emby: not all collections get info on creation, so force it + if builder.obj: + details_list = builder.update_details_emby_with_labels_in_json() if details_list: pre = "" if library.status[str(mapping_name)]["status"] != "Unchanged": pre = f"{library.status[str(mapping_name)]['status']} and " library.status[str(mapping_name)]["status"] = f"{pre}Updated {', '.join(details_list)}" + else: + pass + else: + pass if builder.server_preroll is not None: library.set_server_preroll(builder.server_preroll) @@ -1082,6 +1146,7 @@ def run_playlists(config): status[mapping_name]["status"] = delete_status if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): + builder.is_playlist = True radarr_add, sonarr_add = builder.run_missing() stats["radarr"] += radarr_add status[mapping_name]["radarr"] += radarr_add @@ -1104,7 +1169,7 @@ def run_playlists(config): logger.info("") logger.separator("No Playlist to Update", space=False, border=False) else: - details_list = builder.update_details() + details_list = builder.update_details_emby_with_labels_in_json() if details_list: pre = "" if status[mapping_name]["status"] != "Unchanged": diff --git a/mkdocs.yml b/mkdocs.yml index 276c74611..6c821b5ce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,21 +14,21 @@ theme: text: DM Sans code: Fira Code features: - - content.code.annotate - - content.tabs.link - - content.code.copy - - header.autohide - - announce.dismiss - - navigation.footer - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.top - - navigation.tracking - - toc.follow - - search.suggest - - search.highlight - - search.share + - content.code.annotate + - content.tabs.link + - content.code.copy + - header.autohide + - announce.dismiss + - navigation.footer + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.top + - navigation.tracking + - toc.follow + - search.suggest + - search.highlight + - search.share language: en palette: scheme: slate @@ -36,312 +36,421 @@ theme: icon: alternate: material/file-document-multiple-outline plugins: - - glightbox - - search: - lang: en - - open-in-new-tab - - include-markdown - - redirects: - redirect_maps: - 'discord.md': 'https://kometa.wiki/en/nightly/link/' - 'redact.md': 'https://regex101.com/r/DMo1DQ/latest' - 'link.md': 'https://discord.com/invite/kometa-822460010649878528' - 'malauth.md': 'config/authentication.md' - 'traktauth.md': 'config/authentication.md' - 'kometa/install/local.md': 'kometa/install/walkthroughs/local.md' - 'kometa/install/docker.md': 'kometa/install/walkthroughs/docker.md' - 'kometa/install/unraid.md': 'kometa/install/walkthroughs/unraid.md' - 'kometa/install/kubernetes.md': 'kometa/install/walkthroughs/kubernetes.md' - 'kometa/install/qnap.md': 'kometa/install/walkthroughs/qnap.md' - 'kometa/install/synology.md': 'kometa/install/walkthroughs/synology.md' - - material-plausible +- glightbox +- search: + lang: en +- open-in-new-tab +- include-markdown +- redirects: + redirect_maps: + 'discord.md': 'https://kometa.wiki/en/nightly/link/' + 'redact.md': 'https://regex101.com/r/DMo1DQ/latest' + 'link.md': 'https://discord.com/invite/kometa-822460010649878528' + 'malauth.md': 'config/authentication.md' + 'traktauth.md': 'config/authentication.md' + 'kometa/install/local.md': 'kometa/install/walkthroughs/local.md' + 'kometa/install/docker.md': 'kometa/install/walkthroughs/docker.md' + 'kometa/install/unraid.md': 'kometa/install/walkthroughs/unraid.md' + 'kometa/install/kubernetes.md': 'kometa/install/walkthroughs/kubernetes.md' + 'kometa/install/qnap.md': 'kometa/install/walkthroughs/qnap.md' + 'kometa/install/synology.md': 'kometa/install/walkthroughs/synology.md' +- material-plausible extra: alternate: - - name: Latest Docs - link: /en/latest/ - lang: en - - name: Develop Docs - link: /en/develop/ - lang: en - - name: Nightly Docs - link: /en/nightly/ - lang: en + - name: Latest Docs + link: /en/latest/ + lang: en + - name: Develop Docs + link: /en/develop/ + lang: en + - name: Nightly Docs + link: /en/nightly/ + lang: en social: - - icon: fontawesome/solid/heart - class: md-social__link heart - link: 'https://github.com/sponsors/meisnate12' - name: Donate - - icon: fontawesome/brands/discord - class: md-social__link - link: 'https://kometa.wiki/en/latest/discord/' - name: Discord - - icon: fontawesome/brands/github - class: md-social__link - link: 'https://github.com/Kometa-Team/Kometa' - name: GitHub + - icon: fontawesome/solid/heart + class: md-social__link heart + link: 'https://github.com/sponsors/meisnate12' + name: Donate + - icon: fontawesome/brands/discord + class: md-social__link + link: 'https://kometa.wiki/en/latest/discord/' + name: Discord + - icon: fontawesome/brands/github + class: md-social__link + link: 'https://github.com/Kometa-Team/Kometa' + name: GitHub analytics: provider: plausible domain: kometa.wiki/en/latest src: "https://analytics.kometa.wiki/js/script.js" markdown_extensions: - - abbr - - admonition - - attr_list - - md_in_html - - neoteroi.cards - - pymdownx.details - - pymdownx.emoji: - emoji_generator: !!python/name:material.extensions.emoji.to_svg - emoji_index: !!python/name:material.extensions.emoji.twemoji - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - slugify: !!python/object/apply:pymdownx.slugs.slugify - kwds: - case: lower - - pymdownx.tasklist: - custom_checkbox: true - - pymdownx.tilde - - tables - - toc: - permalink: true - toc_depth: 3 +- abbr +- admonition +- attr_list +- md_in_html +- neoteroi.cards +- pymdownx.details +- pymdownx.emoji: + emoji_generator: !!python/name:material.extensions.emoji.to_svg "" + emoji_index: !!python/name:material.extensions.emoji.twemoji "" +- pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true +- pymdownx.inlinehilite +- pymdownx.snippets +- pymdownx.superfences +- pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower +- pymdownx.tasklist: + custom_checkbox: true +- pymdownx.tilde +- tables +- toc: + permalink: true + toc_depth: 3 extra_css: - - assets/css/extra.css - +- assets/css/extra.css +- https://unpkg.com/tippy.js@6/dist/tippy.css extra_javascript: - - assets/js/extra.js - +- assets/js/extra.js +- https://unpkg.com/@popperjs/core@2 +- https://unpkg.com/tippy.js@6 hooks: - - docs/hooks/copyright.py +- docs/hooks/copyright.py nav: - - KOMETA: - - Home: index.md - - INSTALLATION: - - Installing Kometa: kometa/install/overview.md - - Docker Images: kometa/install/images.md - - Getting Started: kometa/install/getting_started.md - - Configuring Kometa: kometa/install/files.md - - Walkthroughs: - - Local (Win/Mac/Linux): kometa/install/walkthroughs/local.md - - Docker: kometa/install/walkthroughs/docker.md - - unRAID: kometa/install/walkthroughs/unraid.md - - Kubernetes: kometa/install/walkthroughs/kubernetes.md - - QNAP: kometa/install/walkthroughs/qnap.md - - Synology: kometa/install/walkthroughs/synology.md - - POST-INSTALL: - - Run Commands & Env Variables: kometa/environmental.md - - YAML Files Explained: kometa/yaml.md - - Log Files & Common Errors: kometa/logs.md - - Frequently Asked Questions: kometa/faqs.md - - Explanation Guides: &guides - - Overview: kometa/guides/overview.md - - Plex Ratings Explained: kometa/guides/ratings.md - - Kometa Sorting Guide: kometa/guides/order.md - - Scheduling Kometa Runs Guide: kometa/guides/scheduling.md - - Image Asset Directory Guide: kometa/guides/assets.md - - Formula 1 Metadata Guide: kometa/guides/formula.md - - MediUX Assets Guide: kometa/guides/mediux.md - - Managing Recommendations: kometa/guides/hub.md - - Reverting Kometa Changes: kometa/guides/revert.md - - Switching from PMM to Kometa: kometa/guides/rebrand.md - - Companion Scripts: &scripts - - Overview: kometa/scripts/overview.md - - ImageMaid: kometa/scripts/imagemaid.md - - Kometa Overlay Reset: kometa/scripts/overlay-reset.md - - USEFUL LINKS: &links - - Feature Requests: https://features.kometa.wiki/ - - Bugs & Issues: https://github.com/Kometa-Team/Kometa/issues - - Community Configs: https://github.com/Kometa-Team/Community-Configs - - Discord Server: https://kometa.wiki/en/latest/discord/ - - Donate/Sponsor Kometa: https://github.com/sponsors/meisnate12 - - Acknowledgements: kometa/acknowledgements.md - - CONFIGURATION FILE: - - Overview: config/overview.md - - LIBRARIES: - - Library Attributes: config/libraries.md - - File Blocks: config/files.md - - Library Operations: config/operations.md - - CONNECTIONS: - - REQUIRED CONNECTIONS: - - Plex: config/plex.md - - TMDb: config/tmdb.md - - OPTIONAL CONNECTIONS: - - Tautulli: config/tautulli.md - - Github: config/github.md - - OMDb: config/omdb.md - - MDBList: config/mdblist.md - - Notifiarr: config/notifiarr.md - - Gotify: config/gotify.md - - ntfy: config/ntfy.md - - AniDB: config/anidb.md - - Radarr: config/radarr.md - - Sonarr: config/sonarr.md - - Trakt: config/trakt.md - - MyAnimeList: config/myanimelist.md - - Trakt and MyAnimeList Authentication: config/authentication.md - - Webhooks: config/webhooks.md - - OTHER: - - Settings: config/settings.md - - Scheduling Parts: config/schedule.md - - Playlist Files: config/playlists.md - - KOMETA DEFAULTS: - - Defaults Usage Guide: defaults/guide.md - - Defaults Files: defaults/files.md - - COLLECTIONS DEFAULTS: - - Collections: defaults/collections.md - - Separators: defaults/separators.md - - Award: - - Separator: defaults/award/separator.md - - Academy Awards (Oscars): defaults/award/oscars.md - - Berlin Film Festival Awards: defaults/award/berlinale.md - - British Academy of Film Awards: defaults/award/bafta.md - - Cannes Film Festival Awards: defaults/award/cannes.md - - César Awards: defaults/award/cesar.md - - Critics Choice Awards: defaults/award/choice.md - - Emmy Awards: defaults/award/emmy.md - - Golden Globe Awards: defaults/award/golden.md - - Independent Spirit Awards: defaults/award/spirit.md - - National Film Registry: defaults/award/nfr.md - - People's Choice Awards: defaults/award/pca.md - - Razzie Awards: defaults/award/razzie.md - - Screen Actors Guild Awards: defaults/award/sag.md - - Sundance Film Festival Awards: defaults/award/sundance.md - - Toronto International Film Festival: defaults/award/tiff.md - - Venice Film Festival Awards: defaults/award/venice.md - - Chart: - - Separator: defaults/chart/separator.md - - Basic Charts: defaults/chart/basic.md - - AniList Charts: defaults/chart/anilist.md - - IMDb Charts: defaults/chart/imdb.md - - Letterboxd Charts: defaults/chart/letterboxd.md - - MyAnimeList Charts: defaults/chart/myanimelist.md - - Tautulli Charts: defaults/chart/tautulli.md - - TMDb Charts: defaults/chart/tmdb.md - - Trakt Charts: defaults/chart/trakt.md - - Other Charts: defaults/chart/other.md - - Content: - - Genres: defaults/both/genre.md - - Franchises (Movie): defaults/movie/franchise.md - - Franchises (Show): defaults/show/franchise.md - - Universes: defaults/both/universe.md - - Based On...: defaults/both/based.md - - Collectionless: defaults/both/collectionless.md - - Content Rating: - - US Content Ratings (Movie): defaults/movie/content_rating_us.md - - US Content Ratings (Show): defaults/show/content_rating_us.md - - UK Content Ratings: defaults/both/content_rating_uk.md - - DE Content Ratings: defaults/both/content_rating_de.md - - AU Content Ratings: defaults/both/content_rating_au.md - - NZ Content Ratings: defaults/both/content_rating_nz.md - - MyAnimeList Content Ratings: defaults/both/content_rating_mal.md - - Common Sense Media Content Ratings: defaults/both/content_rating_cs.md - - Location: - - Countries (Movie): defaults/movie/country.md - - Countries (Show): defaults/show/country.md - - Regions (Movie): defaults/movie/region.md - - Regions (Show): defaults/show/region.md - - Continents (Movie): defaults/movie/continent.md - - Continents (Show): defaults/show/continent.md - - Media: - - Aspect Ratio: defaults/both/aspect.md - - Resolutions: defaults/both/resolution.md - - Audio Languages: defaults/both/audio_language.md - - Subtitle Languages: defaults/both/subtitle_language.md - - People: - - Actors: defaults/both/actor.md - - Directors: defaults/movie/director.md - - Producers: defaults/movie/producer.md +- KOMETA: + - Home: index.md + - INSTALLATION: + - Installing Kometa: kometa/install/overview.md + - Docker Images: kometa/install/images.md + - Getting Started: kometa/install/getting_started.md + - Configuring Kometa: kometa/install/files.md + - Walkthroughs: + - Local (Win/Mac/Linux): kometa/install/walkthroughs/local.md + - Docker: kometa/install/walkthroughs/docker.md + - unRAID: kometa/install/walkthroughs/unraid.md + - Kubernetes: kometa/install/walkthroughs/kubernetes.md + - QNAP: kometa/install/walkthroughs/qnap.md + - Synology: kometa/install/walkthroughs/synology.md + - TrueNAS: kometa/install/walkthroughs/truenas.md + - POST-INSTALL: + - Run Commands & Env Variables: kometa/environmental.md + - YAML Files Explained: kometa/yaml.md + - Log Files & Common Errors: kometa/logs.md + - Frequently Asked Questions: kometa/faqs.md + - Explanation Guides: &guides + - Overview: kometa/guides/overview.md + - Plex Ratings Explained: kometa/guides/ratings.md + - Overlays Explained: kometa/guides/overlays.md + - Kometa Sorting Guide: kometa/guides/order.md + - Scheduling Kometa Runs Guide: kometa/guides/scheduling.md + - Image Asset Directory Guide: kometa/guides/assets.md + - Formula 1 Metadata Guide: kometa/guides/formula.md + - MediUX Assets Guide: kometa/guides/mediux.md + - Managing Recommendations: kometa/guides/hub.md + - Reverting Kometa Changes: kometa/guides/revert.md + - Switching from PMM to Kometa: kometa/guides/rebrand.md + - Companion Scripts: &scripts + - Overview: kometa/scripts/overview.md + - ImageMaid: kometa/scripts/imagemaid.md + - Kometa Overlay Reset: kometa/scripts/overlay-reset.md + - USEFUL LINKS: &links + - Feature Requests: https://features.kometa.wiki/ + - Bugs & Issues: https://github.com/Kometa-Team/Kometa/issues + - Community Configs: https://github.com/Kometa-Team/Community-Configs + - Discord Server: https://kometa.wiki/en/latest/discord/ + - Donate/Sponsor Kometa: https://github.com/sponsors/meisnate12 + - Acknowledgements: kometa/acknowledgements.md +- CONFIGURATION FILE: + - Overview: config/overview.md + - LIBRARIES: + - Library Attributes: config/libraries.md + - File Blocks: config/files.md + - Library Operations: config/operations.md + - CONNECTIONS: + - REQUIRED CONNECTIONS: + - Plex: config/overview.md + - TMDb: config/tmdb.md + - OPTIONAL CONNECTIONS: + - Tautulli: config/tautulli.md + - Github: config/github.md + - OMDb: config/omdb.md + - MDBList: config/mdblist.md + - Notifiarr: config/notifiarr.md + - Gotify: config/gotify.md + - ntfy: config/ntfy.md + - AniDB: config/anidb.md + - Radarr: config/radarr.md + - Sonarr: config/sonarr.md + - Trakt: config/trakt.md + - MyAnimeList: config/myanimelist.md + - Trakt and MyAnimeList Authentication: config/authentication.md + - Webhooks: config/webhooks.md + - OTHER: + - Settings: config/settings.md + - Scheduling Parts: config/schedule.md + - Playlist Files: config/playlists.md +- KOMETA DEFAULTS: + - Defaults Usage Guide: defaults/guide.md + - Defaults Files: defaults/files.md + - COLLECTIONS DEFAULTS: + - Collections: defaults/collections.md + - Separators: defaults/separators.md + - Award: + - Separator: defaults/award/separator.md + - Academy Awards (Oscars): defaults/award/oscars.md + - Berlin Film Festival Awards: defaults/award/berlinale.md + - British Academy of Film Awards: defaults/award/bafta.md + - Cannes Film Festival Awards: defaults/award/cannes.md + - César Awards: defaults/award/cesar.md + - Critics Choice Awards: defaults/award/choice.md + - Emmy Awards: defaults/award/emmy.md + - Golden Globe Awards: defaults/award/golden.md + - Independent Spirit Awards: defaults/award/spirit.md + - National Film Registry: defaults/award/nfr.md + - People's Choice Awards: defaults/award/pca.md + - Razzie Awards: defaults/award/razzie.md + - Screen Actors Guild Awards: defaults/award/sag.md + - Sundance Film Festival Awards: defaults/award/sundance.md + - Toronto International Film Festival: defaults/award/tiff.md + - Venice Film Festival Awards: defaults/award/venice.md + - Chart: + - Separator: defaults/chart/separator.md + - Basic Charts: defaults/chart/basic.md + - AniList Charts: defaults/chart/anilist.md + - IMDb Charts: defaults/chart/imdb.md + - Letterboxd Charts: defaults/chart/letterboxd.md + - MyAnimeList Charts: defaults/chart/myanimelist.md + - Tautulli Charts: defaults/chart/tautulli.md + - TMDb Charts: defaults/chart/tmdb.md + - Trakt Charts: defaults/chart/trakt.md + - Other Charts: defaults/chart/other.md + - Content: + - Genres: defaults/both/genre.md + - Franchises (Movie): defaults/movie/franchise.md + - Franchises (Show): defaults/show/franchise.md + - Universes: defaults/both/universe.md + - Based On...: defaults/both/based.md + - Collectionless: defaults/both/collectionless.md + - Content Rating: + - US Content Ratings (Movie): defaults/movie/content_rating_us.md + - US Content Ratings (Show): defaults/show/content_rating_us.md + - UK Content Ratings: defaults/both/content_rating_uk.md + - DE Content Ratings: defaults/both/content_rating_de.md + - AU Content Ratings: defaults/both/content_rating_au.md + - NZ Content Ratings: defaults/both/content_rating_nz.md + - MyAnimeList Content Ratings: defaults/both/content_rating_mal.md + - Common Sense Media Content Ratings: defaults/both/content_rating_cs.md + - Location: + - Countries (Movie): defaults/movie/country.md + - Countries (Show): defaults/show/country.md + - Regions (Movie): defaults/movie/region.md + - Regions (Show): defaults/show/region.md + - Continents (Movie): defaults/movie/continent.md + - Continents (Show): defaults/show/continent.md + - Media: + - Aspect Ratio: defaults/both/aspect.md + - Resolutions: defaults/both/resolution.md + - Audio Languages: defaults/both/audio_language.md + - Subtitle Languages: defaults/both/subtitle_language.md + - People: + - Actors: defaults/both/actor.md + - Directors: defaults/movie/director.md + - Producers: defaults/movie/producer.md + - Composers: defaults/movie/composer.md - Writers: defaults/movie/writer.md - - Production: - - Networks: defaults/show/network.md - - Streaming: defaults/both/streaming.md - - Studios: defaults/both/studio.md - - Time: - - Seasonal: defaults/movie/seasonal.md - - Years: defaults/both/year.md - - Decades (Movie): defaults/movie/decade.md - - Decades (Show): defaults/show/decade.md - - OVERLAY DEFAULTS: - - Overlays: defaults/overlays.md + - Production: + - Networks: defaults/show/network.md + - Streaming: defaults/both/streaming.md + - Studios: defaults/both/studio.md + - Time: + - Seasonal: defaults/movie/seasonal.md + - Years: defaults/both/year.md + - Decades (Movie): defaults/movie/decade.md + - Decades (Show): defaults/show/decade.md + - OVERLAY DEFAULTS: + - Overlays: defaults/overlays.md + - Chart: + - Ribbon: defaults/overlays/ribbon.md + - Content: + - Episode Info: defaults/overlays/episode_info.md + - MediaStinger: defaults/overlays/mediastinger.md + - Ratings: defaults/overlays/ratings.md + - Status: defaults/overlays/status.md + - Content Rating: + - US Content Ratings (Movie): defaults/overlays/content_rating_us_movie.md + - US Content Ratings (Show): defaults/overlays/content_rating_us_show.md + - UK Content Ratings: defaults/overlays/content_rating_uk.md + - DE Content Ratings: defaults/overlays/content_rating_de.md + - AU Content Ratings: defaults/overlays/content_rating_au.md + - NZ Content Ratings: defaults/overlays/content_rating_nz.md + - Common Sense Age Ratings: defaults/overlays/commonsense.md + - Media: + - Aspect Ratio: defaults/overlays/aspect.md + - Audio Codec: defaults/overlays/audio_codec.md + - Audio/Subtitle Language Count: defaults/overlays/language_count.md + - Audio/Subtitle Language Flags: defaults/overlays/languages.md + - Resolution/Edition: defaults/overlays/resolution.md + - Runtimes: defaults/overlays/runtimes.md + - Versions: defaults/overlays/versions.md + - Video Format: defaults/overlays/video_format.md + - Production: + - Networks: defaults/overlays/network.md + - Streaming: defaults/overlays/streaming.md + - Studios: defaults/overlays/studio.md + - Utility: + - Direct Play Only: defaults/overlays/direct_play.md + - PLAYLIST DEFAULTS: + - Playlists: defaults/playlist.md +- FILES & BUILDERS: + - Overview: files/overview.md + - FILES: + - Collection Files: files/collections.md + - Overlay Files: files/overlays.md + - Playlist Files: files/playlists.md + - Metadata Files: files/metadata.md + - Definition Templates: files/templates.md + - Dynamic Collections: files/dynamic.md + - Dynamic Collection Types & Data: files/dynamic_types.md + - DEFINITION ATTRIBUTES: + - Builders: + - Overview: files/builders/overview.md + - Plex: + - Overview: files/builders/plex/overview.md + - Smart Filter: files/builders/plex/smart-filter.md + - All: files/builders/plex/all.md + - Collectionless: files/builders/plex/collectionless.md + - Pilots: files/builders/plex/pilots.md + - Search: files/builders/plex/search.md + - Watchlist: files/builders/plex/watchlist.md + - TMDb: + - Overview: files/builders/tmdb/overview.md + - Standard: + - Collection: files/builders/tmdb/collection.md + - Company: files/builders/tmdb/company.md + - Keyword: files/builders/tmdb/keyword.md + - List: files/builders/tmdb/list.md + - Movie: files/builders/tmdb/movie.md + - Network: files/builders/tmdb/network.md + - Show: files/builders/tmdb/show.md - Chart: - - Ribbon: defaults/overlays/ribbon.md - - Content: - - Episode Info: defaults/overlays/episode_info.md - - MediaStinger: defaults/overlays/mediastinger.md - - Ratings: defaults/overlays/ratings.md - - Status: defaults/overlays/status.md - - Content Rating: - - US Content Ratings (Movie): defaults/overlays/content_rating_us_movie.md - - US Content Ratings (Show): defaults/overlays/content_rating_us_show.md - - UK Content Ratings: defaults/overlays/content_rating_uk.md - - DE Content Ratings: defaults/overlays/content_rating_de.md - - AU Content Ratings: defaults/overlays/content_rating_au.md - - NZ Content Ratings: defaults/overlays/content_rating_nz.md - - Common Sense Age Ratings: defaults/overlays/commonsense.md - - Media: - - Aspect Ratio: defaults/overlays/aspect.md - - Audio Codec: defaults/overlays/audio_codec.md - - Audio/Subtitle Language Count: defaults/overlays/language_count.md - - Audio/Subtitle Language Flags: defaults/overlays/languages.md - - Resolution/Edition: defaults/overlays/resolution.md - - Runtimes: defaults/overlays/runtimes.md - - Versions: defaults/overlays/versions.md - - Video Format: defaults/overlays/video_format.md - - Production: - - Networks: defaults/overlays/network.md - - Streaming: defaults/overlays/streaming.md - - Studios: defaults/overlays/studio.md - - Utility: - - Direct Play Only: defaults/overlays/direct_play.md - - PLAYLIST DEFAULTS: - - Playlists: defaults/playlist.md - - FILES & BUILDERS: - - Overview: files/overview.md - - FILES: - - Collection Files: files/collections.md - - Overlay Files: files/overlays.md - - Playlist Files: files/playlists.md - - Metadata Files: files/metadata.md - - Definition Templates: files/templates.md - - Dynamic Collections: files/dynamic.md - - Dynamic Collection Types & Data: files/dynamic_types.md - - DEFINITION ATTRIBUTES: - - Builders: - - Overview: files/builders/overview.md - - Plex Builders: files/builders/plex.md - - TMDb Builders: files/builders/tmdb.md - - TVDb Builders: files/builders/tvdb.md - - IMDb Builders: files/builders/imdb.md - - Trakt Builders: files/builders/trakt.md - - Tautulli Builders: files/builders/tautulli.md - - Radarr Builders: files/builders/radarr.md - - Sonarr Builders: files/builders/sonarr.md - - MDBList Builders: files/builders/mdblist.md - - Letterboxd Builders: files/builders/letterboxd.md - - ICheckMovies Builders: files/builders/icheckmovies.md - - BoxOfficeMojo Builders: files/builders/mojo.md - - Reciperr Builders: files/builders/reciperr.md - - StevenLu Builders: files/builders/stevenlu.md - - AniDB Builders: files/builders/anidb.md - - AniList Builders: files/builders/anilist.md - - MyAnimeList Builders: files/builders/myanimelist.md - - Filters: files/filters.md - - Definition Settings: files/settings.md - - Radarr/Sonarr Settings: files/arr.md - - Collection/Playlist Metadata Updates: files/updates.md - - Item Metadata Updates: files/item_updates.md - - MISCELLANEOUS: - - EXPLANATION GUIDES: *guides - - COMPANION SCRIPTS: *scripts - - USEFUL LINKS: *links - - SPONSOR: https://github.com/sponsors/meisnate12 + - Airing Today: files/builders/tmdb/airing-today.md + - Now Playing: files/builders/tmdb/now-playing.md + - On The Air: files/builders/tmdb/on-the-air.md + - Popular: files/builders/tmdb/popular.md + - Top Rated: files/builders/tmdb/top-rated.md + - Trending Daily: files/builders/tmdb/trending-daily.md + - Trending Weekly: files/builders/tmdb/trending-weekly.md + - Upcoming: files/builders/tmdb/upcoming.md + - Discover: + - Movie: files/builders/tmdb/discover/movie.md + - Show: files/builders/tmdb/discover/show.md + - People: + - Actor: files/builders/tmdb/actor.md + - Crew: files/builders/tmdb/crew.md + - Director: files/builders/tmdb/director.md + - Producer: files/builders/tmdb/producer.md + - Writer: files/builders/tmdb/writer.md + - TVDb: + - Overview: files/builders/tvdb/overview.md + - List: files/builders/tvdb/list.md + - Movie: files/builders/tvdb/movie.md + - Show: files/builders/tvdb/show.md + - IMDb: + - Overview: files/builders/imdb/overview.md + - Award: files/builders/imdb/award.md + - Chart: files/builders/imdb/chart.md + - ID: files/builders/imdb/id.md + - List: files/builders/imdb/list.md + - Search: files/builders/imdb/search.md + - Watchlist: files/builders/imdb/watchlist.md + - Trakt: + - Overview: files/builders/trakt/overview.md + - Box Office: files/builders/trakt/box-office.md + - Chart: files/builders/trakt/chart.md + - List: files/builders/trakt/list.md + - Recommendations: files/builders/trakt/recommendations.md + - UserList: files/builders/trakt/userlist.md + - Tautulli: + - Overview: files/builders/tautulli/overview.md + - Popular: files/builders/tautulli/popular.md + - Watched: files/builders/tautulli/watched.md + - Radarr: + - Overview: files/builders/radarr/overview.md + - All: files/builders/radarr/all.md + - Taglist: files/builders/radarr/taglist.md + - Sonarr: + - Overview: files/builders/sonarr/overview.md + - All: files/builders/sonarr/all.md + - Taglist: files/builders/sonarr/taglist.md + - MDBList: + - Overview: files/builders/mdblist/overview.md + - List: files/builders/mdblist/list.md + - Letterboxd: + - Overview: files/builders/letterboxd/overview.md + - List: files/builders/letterboxd/list.md + - ICheckMovies: + - Overview: files/builders/icheckmovies/overview.md + - List: files/builders/icheckmovies/list.md + - BoxOfficeMojo: + - Overview: files/builders/boxofficemojo/overview.md + - All Time: files/builders/boxofficemojo/all-time.md + - Domestic: files/builders/boxofficemojo/domestic.md + - International: files/builders/boxofficemojo/international.md + - Never Hit: files/builders/boxofficemojo/never.md + - Other Records: files/builders/boxofficemojo/record.md + - Worldwide: files/builders/boxofficemojo/world.md + - Reciperr: + - Overview: files/builders/reciperr/overview.md + - List: files/builders/reciperr/list.md + - StevenLu: + - Overview: files/builders/stevenlu/overview.md + - Popular: files/builders/stevenlu/popular.md + - AniDB: + - Overview: files/builders/anidb/overview.md + - ID: files/builders/anidb/id.md + - Popular: files/builders/anidb/popular.md + - Relation: files/builders/anidb/relation.md + - Tag: files/builders/anidb/tag.md + - AniList: + - Overview: files/builders/anilist/overview.md + - ID: files/builders/anilist/id.md + - Popular: files/builders/anilist/popular.md + - Relations: files/builders/anilist/relations.md + - Search: files/builders/anilist/search.md + - Studio: files/builders/anilist/studio.md + - Top Rated: files/builders/anilist/top_rated.md + - Trending: files/builders/anilist/trending.md + - Userlist: files/builders/anilist/userlist.md + - MyAnimeList: + - Overview: files/builders/myanimelist/overview.md + - All: files/builders/myanimelist/all.md + - Favorite: files/builders/myanimelist/favorite.md + - ID: files/builders/myanimelist/id.md + - Movie: files/builders/myanimelist/movie.md + - OVA: files/builders/myanimelist/ova.md + - Popular: files/builders/myanimelist/popular.md + - Search: files/builders/myanimelist/search.md + - Season: files/builders/myanimelist/season.md + - Special: files/builders/myanimelist/special.md + - Suggested: files/builders/myanimelist/suggested.md + - TV: files/builders/myanimelist/tv.md + - Upcoming: files/builders/myanimelist/upcoming.md + - UserList: files/builders/myanimelist/userlist.md + - Filters: files/filters.md + - Definition Settings: files/settings.md + - Radarr/Sonarr Settings: files/arr.md + - Collection/Playlist Metadata Updates: files/updates.md + - Item Metadata Updates: files/item_updates.md +- MISCELLANEOUS: + - EXPLANATION GUIDES: *guides + - COMPANION SCRIPTS: *scripts + - USEFUL LINKS: *links +- SPONSOR: https://github.com/sponsors/meisnate12 # - SHOWCASE: # - Overlays Showcase: showcase/overlays.md diff --git a/modules/builder.py b/modules/builder.py index 41a8933d1..d654334f0 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,4 +1,6 @@ import os, re, time + +import requests from arrapi import ArrException from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta @@ -71,19 +73,19 @@ album_details = ["non_item_remove_label", "item_label", "item_album_sorting"] sub_filters = [ "filepath", "audio_track_title", "subtitle_track_title", "resolution", "audio_language", "subtitle_language", "has_dolby_vision", - "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile", "versions" + "has_hdr", "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile", "versions" ] filters_by_type = { "movie_show_season_episode_artist_album_track": ["title", "summary", "collection", "has_collection", "added", "last_played", "user_rating", "plays", "filepath", "label", "audio_track_title", "subtitle_track_title", "versions"], "movie_show_season_episode_album_track": ["year"], "movie_show_season_episode_artist_album": ["has_overlay"], - "movie_show_season_episode": ["resolution", "audio_language", "subtitle_language", "has_dolby_vision", "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile"], + "movie_show_season_episode": ["resolution", "audio_language", "subtitle_language", "has_dolby_vision", "has_hdr", "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile"], "movie_show_episode_album": ["release", "critic_rating", "history"], "movie_show_episode_track": ["duration"], "movie_show_artist_album": ["genre"], "movie_show_episode": ["actor", "content_rating", "audience_rating"], "movie_show": ["studio", "original_language", "tmdb_vote_count", "tmdb_vote_average", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword", "imdb_keyword"], - "movie_episode": ["director", "producer", "writer"], + "movie_episode": ["director", "producer", "writer", "composer"], "movie_artist": ["country"], "show_artist": ["folder"], "show_season": ["episodes"], @@ -114,11 +116,11 @@ ] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ - "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", + "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "composer", "year", "origin_country", "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre", "imdb_keyword", "tvdb_genre" ] tag_modifiers = ["", ".not", ".regex", ".count_gt", ".count_gte", ".count_lt", ".count_lte"] -boolean_filters = ["has_collection", "has_edition", "has_overlay", "has_dolby_vision", "has_stinger"] +boolean_filters = ["has_collection", "has_edition", "has_overlay", "has_dolby_vision", "has_hdr", "has_stinger"] date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired", "last_episode_aired_or_never"] date_modifiers = ["", ".not", ".before", ".after", ".regex"] number_filters = [ @@ -190,6 +192,7 @@ def __init__(self, config, metadata, name, data, library=None, overlay=None, ext self.library = library self.libraries = [] self.summaries = {} + self.is_playlist = False self.playlist = library is None self.overlay = overlay methods = {m.lower(): m for m in self.data} @@ -368,7 +371,7 @@ def apply_vars(input_str, var_set, var_key, var_limit): en_summary = apply_vars(en_summary, en_vars, en_key, self.limit) if trans_summary: trans_summary = apply_vars(trans_summary, trans_vars, trans_key, self.limit) - + # this del col has no use in Emby due to col names delete_cols = [] if (self.name and self.name != en_name) or (not self.name and trans_name and en_name != trans_name): delete_cols.append(en_name) @@ -396,7 +399,17 @@ def apply_vars(input_str, var_set, var_key, var_limit): if not self.name: self.name = self.mapping_name - + # This code adds additional prefix/icons for Emby + self.original_name = self.name + + self.icon = "" + match self.library.type: + case "Show": + self.icon = "📺 " + case "Movie": + self.icon = "🎥 " + self.name = f"{self.icon}{self.library.name} {self.name}" +# --------- if self.library and self.name not in self.library.collections: self.library.collections.append(self.name) @@ -425,8 +438,9 @@ def apply_vars(input_str, var_set, var_key, var_limit): logger.debug("Validating Method: only_run_on_create") logger.debug(f"Value: {data[methods['only_run_on_create']]}") self.only_run_on_create = util.parse(self.Type, "only_run_on_create", self.data, datatype="bool", methods=methods, default=False) - if self.obj and self.only_run_on_create: - raise NotScheduled("Skipped because only_run_on_create is True and the collection already exists") + # Deactivated for Emby due to lack of auto collection + # if self.obj and self.only_run_on_create: + # raise NotScheduled("Skipped because only_run_on_create is True and the collection already exists") if "allowed_library_types" in methods and "run_definition" not in methods: logger.warning(f"{self.Type} Warning: allowed_library_types will run as run_definition") @@ -444,7 +458,10 @@ def apply_vars(input_str, var_set, var_key, var_limit): raise Failed(f"{self.Type} Error: {library_type} is invalid. Options: true, false, {', '.join(plex.library_types)}") elif library_type == "false": raise NotScheduled(f"Skipped because run_definition is false") - elif library_type != "true" and self.library and library_type != self.library.Plex.type: + #ToDo: + # self.library.lib_type for Emby, + # self.library.Plex.type for Plex + elif library_type != "true" and self.library and library_type != self.library.lib_type: raise NotScheduled(f"Skipped because run_definition library_type: {library_type} doesn't match") if self.playlist: self.builder_level = "item" @@ -494,8 +511,8 @@ def apply_vars(input_str, var_set, var_key, var_limit): if "overlay" in methods: overlay_data = data[methods["overlay"]] else: - overlay_data = str(self.mapping_name) - logger.warning(f"{self.Type} Warning: No overlay attribute using mapping name {self.mapping_name} as the overlay name") + overlay_data = str(self.original_name) + logger.warning(f"{self.Type} Warning: No overlay attribute using mapping name {self.original_name} as the overlay name") suppress = [] if "suppress_overlays" in methods: logger.debug("") @@ -505,7 +522,7 @@ def apply_vars(input_str, var_set, var_key, var_limit): suppress = util.get_list(data[methods["suppress_overlays"]]) else: logger.error(f"Overlay Error: suppress_overlays attribute is blank") - self.overlay = Overlay(config, library, metadata, str(self.mapping_name), overlay_data, suppress, self.builder_level) + self.overlay = Overlay(config, library, metadata, str(self.original_name), overlay_data, suppress, self.builder_level) self.sync_to_users = None self.exclude_users = None @@ -655,7 +672,7 @@ def apply_vars(input_str, var_set, var_key, var_limit): else: logger.debug(f"Value: {self.data[methods['smart_label']]}") if isinstance(self.data[methods["smart_label"]], dict): - _data, replaced = util.replace_label(self.name, self.data[methods["smart_label"]]) + _data, replaced = util.replace_label(self.original_name, self.data[methods["smart_label"]]) if not replaced: raise Failed("Config Error: <<smart_label>> not found in the smart_label attribute data") self.smart_label = _data @@ -715,7 +732,9 @@ def apply_vars(input_str, var_set, var_key, var_limit): for del_col in util.parse(self.Type, "delete_collections_named", self.data, datatype="strlist", methods=methods): try: del_obj = self.library.get_collection(del_col, force_search=True) + self.library.delete(del_obj) + logger.info(f"Collection: {del_obj.title} deleted") except Failed as e: if str(e).startswith("Plex Error: Failed to delete"): @@ -820,6 +839,24 @@ def apply_vars(input_str, var_set, var_key, var_limit): # Added logic to allow names like "50 Cent" to be passed as a string rather than it passing "50" as the ID to use. if tmdb_person.isdigit(): person = self.config.TMDb.get_person(int(tmdb_person)) + first_person = person.name + # found exact match, now get the offset for poster + resu = self.config.TMDb.search_people(first_person) + idx = 0 # Default + if resu and resu.results: + # 1) Exakte ID treffen + found = None + for i, c in enumerate(resu.results): + try: + if c.id == int(tmdb_person): + found = i + break + except Exception: + pass + if found is not None: + idx = found + self.tmdb_person_offset= idx + pass else: results = self.config.TMDb.search_people(tmdb_person) if not results: @@ -1093,6 +1130,8 @@ def apply_vars(input_str, var_set, var_key, var_limit): self._mdblist(method_name, method_data) elif method_name == "filters": self._filters(method_name, method_data) + elif method_name == "postfix": # emby + pass else: raise Failed(f"{self.Type} Error: {method_final} attribute not supported") except Failed as e: @@ -1154,17 +1193,24 @@ def apply_vars(input_str, var_set, var_key, var_limit): or (self.library.Radarr and self.radarr_details["add_missing"]) or (self.library.Sonarr and self.sonarr_details["add_missing"])) if self.build_collection: - if self.obj and ((self.smart and not self.obj.smart) or (not self.smart and self.obj.smart)): - logger.info("") - logger.error(f"{self.Type} Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection") - self.library.delete(self.obj) - self.obj = None - if self.smart: + # ignored for Emby, collection will be re-used + # if self.obj is not None and ((self.smart and not self.obj.smart) or (not self.smart and self.obj.smart)): + # logger.info("") + # logger.error(f"{self.Type} Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection") + # self.beginning_count = len(self.obj.childCount) + # self.library.delete(self.obj) + # self.obj = None + if self.smart: # e.g. people check_url = self.smart_url if self.smart_url else self.smart_label_url - if self.obj: - if check_url != self.library.smart_filter(self.obj): - self.library.update_smart_collection(self.obj, check_url) - logger.info(f"Metadata: Smart Collection updated to {check_url}") + if self.obj and check_url: + self.library.update_smart_collection(self.obj, check_url) + # self.sync_collection() + logger.info(f"Metadata: Smart Collection updated to {check_url}") + # todo needs sort order once available + + # if check_url != self.library.smart_filter(self.obj): + # self.library.update_smart_collection(self.obj, check_url) + # logger.info(f"Metadata: Smart Collection updated to {check_url}") self.beginning_count = len(self.library.fetchItems(check_url)) if check_url else 0 if self.obj: self.exists = True @@ -1216,9 +1262,49 @@ def _poster(self, method_name, method_data): self.config.Requests.get_image(method_data) self.posters[method_name] = method_data except Failed: - logger.warning(f"{self.Type} Warning: No Poster Found at {method_data}") + import urllib.parse + + # insert name with offset + if "Kometa-Team/People-Images" in str(method_data) and self.data.get("tmdb_person", None): + my_tmdb_id = str(self.data.get("tmdb_person")[0]) + if my_tmdb_id in method_data: + offset = "" if self.tmdb_person_offset == 0 else f" ({self.tmdb_person_offset + 1})" + method_data = method_data.replace(my_tmdb_id, f"{self.data.get('key_name').replace(" ", "%20")}{offset}") + pass + # Hole den Pfad aus der URL + parsed_url = urllib.parse.urlparse(method_data) + path = parsed_url.path + + # URL-kodiere den Library-Namen mit vorangestelltem Leerzeichen (entspricht '%20') + # encoded_library_name = urllib.parse.quote(f"{self.icon}{self.library.name} ") + encoded_library_name = urllib.parse.quote(f"{self.library.mapping_name} ") # unicode icon becomes invisible + + # Entferne '%20{self.library.name}' aus dem Pfad + if encoded_library_name in path: + modified_path = path.replace(encoded_library_name, '') + # Baue die neue URL mit dem modifizierten Pfad + modified_method_data = urllib.parse.urlunparse(( + parsed_url.scheme, + parsed_url.netloc, + modified_path, + parsed_url.params, + parsed_url.query, + parsed_url.fragment + )) + try: + if not modified_method_data.startswith("https://theposterdb.com/api/assets/"): + self.config.Requests.get_image(modified_method_data) + self.posters[method_name] = modified_method_data + method_data = modified_method_data # Aktualisiere method_data, falls benötigt + except Failed: + logger.warning(f"{self.Type} Warning: No Poster Found at {modified_method_data}") + else: + self.posters[method_name] = method_data + + # logger.warning(f"{self.Type} Warning: No Poster Found at {method_data}") elif method_name == "tmdb_list_poster": - self.posters[method_name] = self.config.TMDb.get_list(util.regex_first_int(method_data, "TMDb List ID")).poster_url + self.posters[method_name] = self.config.TMDb.get_list( + util.regex_first_int(method_data, "TMDb List ID")).poster_url elif method_name == "tvdb_list_poster": _, poster = self.config.TVDb.get_list_description(method_data) if poster: @@ -1566,13 +1652,13 @@ def _imdb(self, method_name, method_data): except ValueError: raise Failed(f"{self.Type} Error: imdb_award event_year attribute invalid: {og_year}") elif str(og_year).startswith("-"): - event_year = str(self.current_year + int(og_year)) - if event_year not in year_options: - raise Failed(f"{self.Type} Error: imdb_award event_year attribute not an option: {event_year}. Event Options: [{', '.join(year_options)}]") + event_year = [str(self.current_year + int(og_year))] + if event_year[0] not in year_options: + raise Failed(f"{self.Type} Error: imdb_award event_year attribute not an option: {event_year[0]}. Event Options: [{', '.join(year_options)}]") else: event_year = util.parse(self.Type, "event_year", og_year, parent=method_name, datatype="strlist", options=year_options) if (event_year == "all" or len(event_year) > 1) and not git_event: - raise Failed(f"{self.Type} Error: Only specific events work when using multiple years. Event Options: [{', '.join([k for k in self.config.IMDb.events_validation])}]") + raise Failed(f"{self.Type} Error: Only specific events work when using multiple years. Event Options: [{', '.join([k for k in self.config.IMDb.git_events_validation])}]") award_filters = [] if "award_filter" in dict_methods: if not dict_data[dict_methods["award_filter"]]: @@ -1586,19 +1672,22 @@ def _imdb(self, method_name, method_data): final_category = [] final_awards = [] if award_filters or category_filters: - award_names, category_names = self.config.IMDb.get_award_names(event_id, year_options[0] if event_year == "latest" else event_year) - lower_award = {a.lower(): a for a in award_names if a} + award_names, category_names = self.config.IMDb.get_event_names(event_id, year_options[:1] if event_year == "latest" else year_options if event_year == "all" else event_year) for award_filter in award_filters: - if award_filter in lower_award: - final_awards.append(lower_award[award_filter]) + if award_filter in award_names: + final_awards.append(award_filter) else: - raise Failed(f"{self.Type} Error: imdb_award award_filter attribute invalid: {award_filter} must be in in [{', '.join([v for _, v in lower_award.items()])}]") - lower_category = {c.lower(): c for c in category_names if c} + raise Failed(f"{self.Type} Error: imdb_award award_filter attribute invalid: {award_filter} must be in in [{', '.join([v for _, v in award_names.items()])}]") for category_filter in category_filters: - if category_filter in lower_category: - final_category.append(lower_category[category_filter]) + if category_filter in category_names or category_filter.replace(", ", " - ") in category_names: + final_category.append(category_filter) + else: - raise Failed(f"{self.Type} Error: imdb_award category_filter attribute invalid: {category_filter} must be in in [{', '.join([v for _, v in lower_category.items()])}]") + raise Failed( + f"{self.Type} Error: imdb_award category_filter attribute invalid: " + f"{category_filter} must be in [{', '.join(category_names)}]" + ) + # raise Failed(f"{self.Type} Error: imdb_award category_filter attribute invalid: {category_filter} must be in in [{', '.join([v for _, v in category_names.items()])}]") self.builders.append((method_name, { "event_id": event_id, "event_year": event_year, "award_filter": final_awards if final_awards else None, "category_filter": final_category if final_category else None, "winning": util.parse(self.Type, "winning", dict_data, parent=method_name, methods=dict_methods, datatype="bool", default=False) @@ -2215,6 +2304,10 @@ def _filters(self, method_name, method_data): message = f"{self.Type} Error: {filter_final} filter attribute is blank" else: try: + if self.data and "tmdb_person" in self.data: + tmdb_person_id = self.data.get("tmdb_person") + pass + final_data = self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate) except FilterFailed as e: raise Failed(e) @@ -2308,7 +2401,7 @@ def filter_and_save_items(self, ids): found = True rating_keys = pl_library.imdb_map[input_id] break - if not found and (self.builder_level == "episode" or self.playlist or self.do_missing): + if not found: try: _id, tmdb_type = self.config.Convert.imdb_to_tmdb(input_id, fail=True) if tmdb_type == "episode" and (self.builder_level == "episode" or self.playlist): @@ -2344,14 +2437,22 @@ def filter_and_save_items(self, ids): self.missing_shows.append(tvdb_id) elif tmdb_type == "movie" and self.do_missing and _id not in self.missing_movies: self.missing_movies.append(_id) - elif tmdb_type in ["show", "episode"] and self.do_missing: + elif tmdb_type in ["show", "episode"]: if tmdb_type == "episode": tmdb_id, _, _ = _id.split("_") else: tmdb_id = _id tvdb_id = self.config.Convert.tmdb_to_tvdb(tmdb_id, fail=True) - if tvdb_id not in self.missing_shows: - self.missing_shows.append(tvdb_id) + if tvdb_id not in self.ignore_ids: + found_keys = None + for pl_library in self.libraries: + if tvdb_id in pl_library.show_map: + found_keys = pl_library.show_map[tvdb_id] + break + if found_keys: + rating_keys = found_keys + elif self.do_missing and tvdb_id not in self.missing_shows: + self.missing_shows.append(tvdb_id) except Failed as e: logger.warning(e) continue @@ -2488,7 +2589,9 @@ def filter_and_save_items(self, ids): if not isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)): logger.error(f"{self.Type} Error: Item: {item} is an invalid type") continue - if item not in self.found_items: + # if item not in self.found_items: + if item.ratingKey not in [found_item.ratingKey for found_item in self.found_items]: + # Weiteres Processing if item.ratingKey in self.filtered_keys: if self.details["show_filtered"] is True: logger.info(f"{name} {self.Type} | X | {self.filtered_keys[item.ratingKey]}") @@ -2642,6 +2745,7 @@ def build_url_arg(arg, mod=None, arg_s=None, mod_s=None): if search_mod == "o": validation = f"{validation[:-1]}mon" results, display_add = build_url_arg(f"-{validation}", mod=last_mod, arg_s=f"{validation} {plex.date_sub_mods[search_mod]}", mod_s=last_text) + pass elif attr == "duration" and modifier in [".gt", ".gte", ".lt", ".lte"]: results, display_add = build_url_arg(validation * 60000) elif modifier == ".rated": @@ -2797,7 +2901,12 @@ def smart_pair(list_to_pair): final_values.append(value) else: final_values = util.get_list(data, trim=False) - search_choices, names = self.library.get_search_choices(attribute, title=not plex_search) + tmdb_person_id = None + if self.data and "tmdb_person" in self.data: + tmdb_person_id = self.data.get("tmdb_person")[0] + pass + + search_choices, names = self.library.get_search_choices(attribute, title=not plex_search, person_list = final_values, tmdb_person_id = tmdb_person_id) valid_list = [] for fvalue in final_values: if str(fvalue) in search_choices or str(fvalue).lower() in search_choices: @@ -2805,7 +2914,7 @@ def smart_pair(list_to_pair): valid_list.append((fvalue, valid_value) if plex_search else valid_value) else: actor_id = None - if attribute in ["actor", "director", "producer", "writer"]: + if attribute in ["actor", "director", "producer", "writer", "composer"]: actor_id = self.library.get_actor_id(fvalue) if actor_id: if plex_search: @@ -2813,7 +2922,7 @@ def smart_pair(list_to_pair): else: valid_list.append(actor_id) if not actor_id: - error = f"Plex Error: {attribute}: {fvalue} not found" + error = f"Emby Error: {attribute}: {fvalue} not found" if self.details["show_options"]: error += f"\nOptions: {names}" if validate: @@ -2880,21 +2989,28 @@ def add_to_collection(self): logger.info("") logger.separator(f"Adding to {self.name} {self.Type}", space=False, border=False) logger.info("") - name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection) + try: + name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection) + except: + name = self.obj if self.obj else self.name + collection_items =[] total = self.limit if self.limit and len(self.found_items) > self.limit else len(self.found_items) spacing = len(str(total)) * 2 + 1 amount_added = 0 amount_unchanged = 0 items_added = [] + collection_items_keys = [key.ratingKey for key in collection_items] for i, item in enumerate(self.found_items, 1): if self.limit and amount_added + self.beginning_count - len([r for _, r in self.remove_item_map.items() if r is not None]) >= self.limit: logger.info(f"{self.Type} Limit reached") self.found_items = self.found_items[:i - 1] break - current_operation = "=" if item in collection_items else "+" + current_operation = "=" if item.ratingKey in collection_items_keys else "+" + # current_operation = "=" if item in collection_items else "+" number_text = f"{i}/{total}" logger.info(f"{number_text:>{spacing}} | {name} {self.Type} | {current_operation} | {util.item_title(item)}") - if item in collection_items: + if item.ratingKey in collection_items_keys: + # if item in collection_items: self.remove_item_map[item.ratingKey] = None amount_unchanged += 1 else: @@ -2936,10 +3052,11 @@ def sync_collection(self): if self.details["changes_webhooks"]: self.notification_removals.append(util.item_set(item, self.library.get_id_from_maps(item.ratingKey))) if self.playlist and items_removed: - self.library.item_reload(self.obj) + # emby + # self.library.item_reload(self.obj) self.obj.removeItems(items_removed) elif items_removed: - self.library.alter_collection(items_removed, self.name, smart_label_collection=self.smart_label_collection, add=False) + self.library.alter_collection(items_removed, self.name, smart_label_collection=self.smart_label_collection, add=False, collection_id = self.obj.ratingKey) if self.do_report and items_removed: self.library.add_removed(self.name, [(i.title, self.library.get_id_from_maps(i.ratingKey)) for i in items_removed], self.library.is_movie) logger.info("") @@ -2967,15 +3084,15 @@ def check_imdb_filters(self, imdb_info, filters_in): return False return True - def check_missing_filters(self, item_id, is_movie, tmdb_item=None, check_released=False): + def check_missing_filters(self, tmdb_id, is_movie, tmdb_item=None, check_released=False): imdb_info = None if self.has_tmdb_filters or self.has_imdb_filters or check_released: try: if tmdb_item is None: if is_movie: - tmdb_item = self.config.TMDb.get_movie(item_id, ignore_cache=True) + tmdb_item = self.config.TMDb.get_movie(tmdb_id, ignore_cache=True) else: - tmdb_item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id, fail=True), ignore_cache=True) + tmdb_item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(tmdb_id, fail=True), ignore_cache=True) except Failed: return False if self.has_imdb_filters and tmdb_item and tmdb_item.imdb_id: @@ -3014,7 +3131,7 @@ def check_filters(self, item, display): final_return = True if self.filters and not self.details["only_filter_missing"]: logger.ghost(f"Filtering {display} {item.title}") - item = self.library.reload(item) + # item = self.library.reload(item) final_return = False tmdb_item = None tvdb_item = None @@ -3092,7 +3209,7 @@ def display_filters(self): def run_missing(self): added_to_radarr = 0 added_to_sonarr = 0 - if len(self.missing_movies) > 0 and self.library.is_movie: + if len(self.missing_movies) > 0 and (self.library.is_movie or self.is_playlist): if self.details["show_missing"] is True: logger.info("") logger.separator(f"Missing Movies from Library: {self.library.name}", space=False, border=False) @@ -3144,7 +3261,7 @@ def run_missing(self): self.run_again_movies.extend(missing_tmdb_ids) if len(filtered_movies_with_names) > 0 and self.do_report: self.library.add_filtered(self.name, filtered_movies_with_names, True) - if len(self.missing_shows) > 0 and self.library.is_show: + if len(self.missing_shows) > 0 and (self.library.is_show or self.is_playlist): if self.details["show_missing"] is True: logger.info("") logger.separator(f"Missing Shows from Library: {self.name}", space=False, border=False) @@ -3204,7 +3321,7 @@ def run_missing(self): return added_to_radarr, added_to_sonarr def load_collection_items(self): - if self.build_collection and self.obj: + if self.build_collection and self.obj is not None: self.items = self.library.get_collection_items(self.obj, self.smart_label_collection) elif not self.build_collection: logger.info("") @@ -3236,8 +3353,9 @@ def update_item_details(self): tmdb_paths = [] tvdb_paths = [] for item in self.items: - item = self.library.reload(item) + # item = self.library.reload(item) current_labels = [la.tag for la in self.library.item_labels(item)] + # todo: check for file, no overlay in tags if "item_assets" in self.item_details and self.asset_directory and "Overlay" not in current_labels: self.library.find_and_upload_assets(item, current_labels, asset_directory=self.asset_directory) self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags) @@ -3283,7 +3401,9 @@ def update_item_details(self): names = {s.season_number: s.name for s in self.config.TMDb.get_show(tmdb_id).seasons} for season in self.library.query(item.seasons): if season.index in names and season.title != names[season.index]: - season.editTitle(names[season.index]) + # season.editTitle(names[season.index]) + self.library.EmbyServer.editItemTitle(season.ratingKey, names[season.index]) + except Failed as e: logger.error(e) @@ -3332,21 +3452,43 @@ def update_item_details(self): logger.error(f"Arr Error: {e}") def load_collection(self): + emby_id = None if self.obj is None and self.smart_url: - self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url, self.ignore_blank_results) + emby_id = self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url, self.ignore_blank_results, self.minimum) + # mycol_id = self.library.EmbyServer.get_collection_id(self.name) + # mycol = self.library.EmbyServer.get_item(mycol_id) + # self.obj = self.library.EmbyServer.convert_emby_to_plex([mycol])[0] logger.debug(f"Smart Collection Created: {self.smart_url}") elif self.obj is None and self.blank_collection: - self.library.create_blank_collection(self.name) + emby_id = self.library.create_blank_collection(self.name) + elif self.smart_label_collection: try: if not self.library.smart_label_check(self.name): raise Failed smart_type, _, self.smart_url = self.build_filter("smart_label", self.smart_label, default_sort="random") - if not self.obj: - self.library.create_smart_collection(self.name, smart_type, self.smart_url, self.ignore_blank_results) + emby_col_id = self.library.EmbyServer.get_collection_id(self.name) + if emby_col_id: + new_col = self.library.EmbyServer.get_item(emby_col_id) + self.obj = self.library.EmbyServer.convert_emby_to_plex([new_col])[0] + if self.obj is None: + emby_id= self.library.create_smart_collection(self.name, smart_type, self.smart_url, self.ignore_blank_results, minimum = self.minimum) except Failed: raise Failed(f"{self.Type} Error: Label: {self.name} was not added to any items in the Library") - self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name, force_search=True) + + if emby_id is None: + emby_id = self.library.EmbyServer.get_collection_id(self.name) + + if self.obj is None and emby_id is not None: + new_col = self.library.EmbyServer.get_item(emby_id) + if new_col is not None: + self.obj = self.library.EmbyServer.convert_emby_to_plex([new_col])[0] + + + # try: + # self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name, force_search=True) + # except Failed: + # self.obj = None if not self.exists: self.created = True @@ -3393,7 +3535,8 @@ def update_details(self): logger.error("Metadata: Failed to Update Please delete the collection and run again") logger.info("") else: - self.library.item_reload(self.obj) + # emby + # self.library.item_reload(self.obj) #self.obj.batchEdits() batch_display = "Collection Metadata Edits" if summary[1] and str(summary[1]) != str(self.obj.summary): @@ -3408,6 +3551,7 @@ def update_details(self): if title.startswith(f"{op} "): title = f"{title[len(op):].strip()}, {op.strip()}" break + new_sort_title = new_sort_title.replace("<<title>>", title) if new_sort_title != str(self.obj.titleSort): self.obj.editSortTitle(new_sort_title) @@ -3495,7 +3639,7 @@ def update_details(self): if self.details["name_mapping"]: name_mapping = self.details["name_mapping"] else: logger.error(f"{self.Type} Error: name_mapping attribute is blank") try: - asset_poster, asset_background, asset_location, _ = self.library.find_item_assets(name_mapping, asset_directory=self.asset_directory) + asset_poster, asset_background, _, asset_location, _ = self.library.find_item_assets(name_mapping, asset_directory=self.asset_directory) if asset_poster: self.posters["asset_directory"] = asset_poster if asset_background: @@ -3503,8 +3647,8 @@ def update_details(self): except Failed as e: if self.library.asset_folders and (self.library.show_missing_assets or self.library.create_asset_folders): logger.warning(e) - if self.mapping_name in self.library.collection_images or self.name in self.library.collection_images: - style_data = self.library.collection_images[self.mapping_name if self.mapping_name in self.library.collection_images else self.name] + if self.mapping_name in self.library.collection_images or self.original_name in self.library.collection_images: + style_data = self.library.collection_images[self.mapping_name if self.mapping_name in self.library.collection_images else self.original_name] if style_data and "url_poster" in style_data and style_data["url_poster"]: self.posters["style_data"] = style_data["url_poster"] elif style_data and "tpdb_poster" in style_data and style_data["tpdb_poster"]: @@ -3514,8 +3658,9 @@ def update_details(self): elif style_data and "tpdb_background" in style_data and style_data["tpdb_background"]: self.backgrounds["style_data"] = f"https://theposterdb.com/api/assets/{style_data['tpdb_background']}" - self.collection_poster = self.library.pick_image(self.obj.title, self.posters, self.library.prioritize_assets, self.library.download_url_assets, asset_location) - self.collection_background = self.library.pick_image(self.obj.title, self.backgrounds, self.library.prioritize_assets, self.library.download_url_assets, asset_location, is_poster=False) + # use the original name for the poster instead of self.obj.title + self.collection_poster = self.library.pick_image(self.original_name, self.posters, self.library.prioritize_assets, self.library.download_url_assets, asset_location) + self.collection_background = self.library.pick_image(self.obj.title, self.backgrounds, self.library.prioritize_assets, self.library.download_url_assets, asset_location, image_type="background") clean_temp = False if isinstance(self.collection_poster, KometaImage): @@ -3524,7 +3669,7 @@ def update_details(self): self.collection_poster = self.collection_poster.save(item_vars) if self.collection_poster or self.collection_background: - pu, bu = self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background) + pu, bu, lu = self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background) if pu or bu: updated_details.append("Image") @@ -3541,7 +3686,315 @@ def update_details(self): self.library.upload_theme(self.obj, filepath=self.file_theme) return updated_details + def update_details_emby_with_labels_in_json(self): + """ + Aktualisiert die Details einer Sammlung in Emby, wobei Tags/Labels über JSON (`kometa_labels`) bearbeitet werden, + und die restlichen Eigenschaften über die Emby-API. + """ + updated_details = [] + logger.info("") + logger.separator(f"Updating EMBY Metadata of {self.name} {self.Type}", space=False, border=False) + logger.info("") + + embyserver = self.library.EmbyServer + + if "summary" in self.summaries: summary = ("summary", self.summaries["summary"]) + elif "translation" in self.summaries: summary = ("translation", self.summaries["translation"]) + elif "tmdb_description" in self.summaries: summary = ("tmdb_description", self.summaries["tmdb_description"]) + elif "tvdb_description" in self.summaries: summary = ("tvdb_description", self.summaries["tvdb_description"]) + elif "letterboxd_description" in self.summaries: summary = ("letterboxd_description", self.summaries["letterboxd_description"]) + elif "tmdb_summary" in self.summaries: summary = ("tmdb_summary", self.summaries["tmdb_summary"]) + elif "tvdb_summary" in self.summaries: summary = ("tvdb_summary", self.summaries["tvdb_summary"]) + elif "tmdb_biography" in self.summaries: summary = ("tmdb_biography", self.summaries["tmdb_biography"]) + elif "tmdb_person" in self.summaries: summary = ("tmdb_person", self.summaries["tmdb_person"]) + elif "tmdb_collection_details" in self.summaries: summary = ("tmdb_collection_details", self.summaries["tmdb_collection_details"]) + elif "trakt_list_details" in self.summaries: summary = ("trakt_list_details", self.summaries["trakt_list_details"]) + elif "tmdb_list_details" in self.summaries: summary = ("tmdb_list_details", self.summaries["tmdb_list_details"]) + elif "tvdb_list_details" in self.summaries: summary = ("tvdb_list_details", self.summaries["tvdb_list_details"]) + elif "letterboxd_list_details" in self.summaries: summary = ("letterboxd_list_details", self.summaries["letterboxd_list_details"]) + elif "icheckmovies_list_details" in self.summaries: summary = ("icheckmovies_list_details", self.summaries["icheckmovies_list_details"]) + elif "tmdb_actor_details" in self.summaries: summary = ("tmdb_actor_details", self.summaries["tmdb_actor_details"]) + elif "tmdb_crew_details" in self.summaries: summary = ("tmdb_crew_details", self.summaries["tmdb_crew_details"]) + elif "tmdb_director_details" in self.summaries: summary = ("tmdb_director_details", self.summaries["tmdb_director_details"]) + elif "tmdb_producer_details" in self.summaries: summary = ("tmdb_producer_details", self.summaries["tmdb_producer_details"]) + elif "tmdb_writer_details" in self.summaries: summary = ("tmdb_writer_details", self.summaries["tmdb_writer_details"]) + elif "tmdb_movie_details" in self.summaries: summary = ("tmdb_movie_details", self.summaries["tmdb_movie_details"]) + elif "tvdb_movie_details" in self.summaries: summary = ("tvdb_movie_details", self.summaries["tvdb_movie_details"]) + elif "tvdb_show_details" in self.summaries: summary = ("tvdb_show_details", self.summaries["tvdb_show_details"]) + elif "tmdb_show_details" in self.summaries: summary = ("tmdb_show_details", self.summaries["tmdb_show_details"]) + else: summary = (None, None) + # bug: list object coming in from somewhere "sight & sound" + # Overview (Summary) aktualisieren + # print(self.obj) + + if self.playlist: + if summary[1]: + if str(summary[1]) != str(self.obj.summary): + try: + # self.obj.editSummary(str(summary[1])) + embyserver.set_item_property(self.obj.ratingKey, "Overview", str(summary[1])) + + logger.info(f"Summary ({summary[0]}) | {summary[1]:<25}") + logger.info("Metadata: Update Completed") + updated_details.append("Metadata") + except NotFound: + logger.error("Metadata: Failed to Update Please delete the collection and run again") + logger.info("") + else: + + batch_display = "Collection Metadata Edits" + new_properties = {} + try: + if summary[1]: + homepage_url = None + # match = re.search(r"<<homepage_url=(https?://[^\]]+)>>", summary[1]) + # if match: + # homepage_url = match.group(1) + # Entferne die URL aus der Summary + # cleaned_summary = re.sub(r"<<homepage_url=https?://[^\]]+>>\n?", "", summary[1]) + + if str(summary[1]) != str(self.obj.summary): + # self.obj.editSummary(summary[1]) + # embyserver.set_item_property(self.obj.ratingKey, "Overview", summary[1]) + new_properties["Overview"] = summary[1] + batch_display += f"\nSummary ({summary[0]}) | {summary[1]:<25}" + # if homepage_url: + # new_properties["ProviderIds"] = {"Website": homepage_url, + # "Homepage": homepage_url, + # "Official Website": homepage_url} + except: + raise Warning(f"ERROR: {self.obj} is not a Plex object.") + # homepage_url = xxx + # SortName (Sortiertitel) aktualisieren + # print(self.obj) + if "sort_title" in self.details: + # todo: add current emby sort title + sort_title = str(self.details["sort_title"]).replace("<<title>>", self.name) + new_sort_title = str(self.details["sort_title"]) + + # '!130_📺 Serien Golden Globe 2023' + # '!130_📺 Serien Emmys 2023' + # '!130_📺 Serien Emmys 2024' + # '!130_Emmys !' + + if "<<title>>" in new_sort_title: + title = self.name + for op in ["The ", "A ", "An "]: + if title.startswith(f"{op} "): + title = f"{title[len(op):].strip()}, {op.strip()}" + break + new_sort_title = new_sort_title.replace("<<title>>", title) + else: + parts = new_sort_title.split("_", 1) + # expected_prefix = f"{self.icon}{self.library.name} " # entsricht '📺 Serien ' + + # new_sort_title = f"{expected_prefix}_{new_sort_title}" + if False: # OLD + if len(parts) > 1 and not parts[1].startswith(expected_prefix) and not parts[1][1:].startswith(f"!{expected_prefix}"): + # Ergänzen, falls der gewünschte Prefix nicht vorhanden ist + new_sort_title = f"{parts[0]}_{expected_prefix}{parts[1]}" + # elif len(parts) == 1: # Kein Unterstrich vorhanden, einfach anhängen + # new_sort_title = f"{new_sort_title}_{expected_prefix}" + + if new_sort_title.endswith(" !"): + new_sort_title = new_sort_title[:-2] + + expected_prefix = f"{self.icon}{self.library.name}" # entsricht '📺 Serien ' + + # Put the library name in front for better sorting of Emnby collections + new_sort_title = f"{expected_prefix}_{new_sort_title.replace(f"{self.icon}{self.library.name} ", "")}" + + # append icon for filtering the libraries + if False: # OLD + new_sort_title = f"{self.icon}{new_sort_title}" + + if new_sort_title != sort_title: + batch_display += f"\nSort Title | {new_sort_title}" + # Konstruiere die URL für den API-Request + # print(self.obj['Id']) + # print(self.obj.ratingKey) + new_properties["ForcedSortName"] = new_sort_title + # embyserver.set_item_property(self.obj.ratingKey, "ForcedSortName", new_sort_title) + + + # todo add content rating + # if "content_rating" in self.details and str(self.details["content_rating"]) != str(self.obj.contentRating): + # self.obj.editContentRating(self.details["content_rating"]) + # batch_display += f"\nContent Rating | {self.details['content_rating']}" + + if len(new_properties.items()) > 0: + embyserver.update_item(self.obj.ratingKey, new_properties) + + # Tags/Labels in JSON aktualisieren + + add_tags = self.details["label"] if "label" in self.details else [] + remove_tags = self.details["label.remove"] if "label.remove" in self.details else None + sync_tags = self.details["label.sync"] if "label.sync" in self.details else None + if sync_tags: + sync_tags.append("Kometa") + else: + add_tags.append("Kometa") + + tag_results = self.library.edit_tags('label', self.obj, add_tags=add_tags, remove_tags=remove_tags, + sync_tags=sync_tags, do_print=False) + if tag_results: + batch_display += f"\n{tag_results}" + + logger.info(batch_display) + if len(batch_display) > 25: + try: + # self.obj.saveEdits() + logger.info("Metadata: Update Completed") + updated_details.append("Metadata") + except NotFound: + logger.error("Metadata: Failed to Update Please delete the collection and run again") + logger.info("") + + + # print(f"EMBY LABELS: {self.obj} - {label_data}") + + # Speichern der Label-Informationen in der JSON-Datei + collection_id = self.obj.ratingKey + # current_tags = self.details["label"] + + # ----------- + # todo add / rewrite + + if False: + advance_update = False + if "collection_mode" in self.details: + if (self.blank_collection and self.created) or int(self.obj.collectionMode) not in plex.collection_mode_keys \ + or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]: + if self.blank_collection and self.created: + self.library.collection_mode_query(self.obj, "hide") + logger.info(f"Collection Mode | hide") + self.library.collection_mode_query(self.obj, "default") + logger.info(f"Collection Mode | default") + self.library.collection_mode_query(self.obj, self.details["collection_mode"]) + logger.info(f"Collection Mode | {self.details['collection_mode']}") + advance_update = True + + if "collection_filtering" in self.details: + try: + self.library.edit_query(self.obj, {"collectionFilterBasedOnUser": 0 if self.details["collection_filtering"] == "admin" else 1}, advanced=True) + advance_update = True + except NotFound: + logger.error("Collection Error: collection_filtering requires a more recent version of Plex Media Server") + + if "collection_order" in self.details: + if int(self.obj.collectionSort) not in plex.collection_order_keys \ + or plex.collection_order_keys[int(self.obj.collectionSort)] != self.details["collection_order"]: + self.library.collection_order_query(self.obj, self.details["collection_order"]) + logger.info(f"Collection Order | {self.details['collection_order']}") + advance_update = True + + if "visible_library" in self.details or "visible_home" in self.details or "visible_shared" in self.details: + visibility = self.library.collection_visibility(self.obj) + visible_library = None + visible_home = None + visible_shared = None + + if "visible_library" in self.details and self.details["visible_library"] != visibility["library"]: + visible_library = self.details["visible_library"] + + if "visible_home" in self.details and self.details["visible_home"] != visibility["home"]: + visible_home = self.details["visible_home"] + + if "visible_shared" in self.details and self.details["visible_shared"] != visibility["shared"]: + visible_shared = self.details["visible_shared"] + + if visible_library is not None or visible_home is not None or visible_shared is not None: + self.library.collection_visibility_update(self.obj, visibility=visibility, library=visible_library, home=visible_home, shared=visible_shared) + advance_update = True + logger.info("Collection Visibility Updated") + + if advance_update and "Metadata" not in updated_details: + updated_details.append("Metadata") + + + # ------------- + + + # save_labels_to_file(file_path_kometa, kometa_labels) + # logger.info(f"Labels updated in JSON for Collection ID {collection_id}") + updated_details.append("Tag") + + asset_location = None + if self.asset_directory: + name_mapping = self.name + if "name_mapping" in self.details: + if self.details["name_mapping"]: name_mapping = self.details["name_mapping"] + else: logger.error(f"{self.Type} Error: name_mapping attribute is blank") + try: + # return poster, background, logo, item_asset_directory, folder_name + + asset_poster, asset_background, logo, asset_location, _ = self.library.find_item_assets(name_mapping, asset_directory=self.asset_directory) + if asset_poster: + self.posters["asset_directory"] = asset_poster + if asset_background: + self.backgrounds["asset_directory"] = asset_background + except Failed as e: + if self.library.asset_folders and (self.library.show_missing_assets or self.library.create_asset_folders): + logger.warning(e) + if self.mapping_name in self.library.collection_images or self.original_name in self.library.collection_images: + style_data = self.library.collection_images[self.mapping_name if self.mapping_name in self.library.collection_images else self.original_name] + if style_data and "url_poster" in style_data and style_data["url_poster"]: + self.posters["style_data"] = style_data["url_poster"] + elif style_data and "tpdb_poster" in style_data and style_data["tpdb_poster"]: + self.posters["style_data"] = f"https://theposterdb.com/api/assets/{style_data['tpdb_poster']}" + if style_data and "url_background" in style_data and style_data["url_background"]: + self.backgrounds["style_data"] = style_data["url_background"] + elif style_data and "tpdb_background" in style_data and style_data["tpdb_background"]: + self.backgrounds["style_data"] = f"https://theposterdb.com/api/assets/{style_data['tpdb_background']}" + + # use original name for the poster + self.collection_poster = self.library.pick_image(self.original_name, self.posters, self.library.prioritize_assets, self.library.download_url_assets, asset_location) + self.collection_background = self.library.pick_image(self.original_name, self.backgrounds, self.library.prioritize_assets, self.library.download_url_assets, asset_location, image_type="background") + + # from modules.poster import ImageData + + + # background = ImageData("asset_directory", os.path.abspath(background_matches[0]), prefix=prefix, image_type="background", is_url=False) + # + # logo = ImageData("asset_directory", os.path.abspath(logo_matches[0]), prefix=prefix, image_type="logo", is_url=False) + + + # Bilder (Poster/Backdrop) aktualisieren + if self.collection_poster: + # my_emby = self.library.EmbyServer + # poster = ImageData("asset_directory", os.path.abspath(poster_matches[0]), prefix=prefix, is_url=False) + + uploaded_poster, _, _ = self.library.upload_images(self.obj, poster=self.collection_poster) + + + # uploaded_poster = my_emby.set_image_smart(self.obj.ratingKey, self.collection_poster.location) + + # if uploaded_poster: + # logger.info(f"Poster updated: {self.collection_poster.location}") + # updated_details.append("Image") + # else: + # logger.info(f"Poster update not needed.") + + + if self.collection_background and not "BackdropImageTags" in self.library.EmbyServer.get_item(self.obj.ratingKey): + + _, background_uploaded, _ = self.library.upload_images(self.obj, background=self.collection_background) + + + # uploaded = self.library.EmbyServer.set_image_smart(self.obj.ratingKey,self.collection_background.location, image_type="Backdrop")#.emby_server_url}/Items/{self.obj.ratingKey}/Images/Backdrop" + # files = {"image": open(self.collection_background, "rb")} + # response = requests.post(url, headers=headers, files=files) + if background_uploaded: + logger.info(f"Backdrop updated: {self.collection_background.location}") + updated_details.append("Image") + else: + logger.warning(f"Failed to update Backdrop: {response.text}") + + return updated_details + def sort_collection(self): + #emby + return logger.info("") logger.separator(f"Sorting {self.name} {self.Type}", space=False, border=False) logger.info("") @@ -3564,6 +4017,7 @@ def sort_collection(self): else: raise Failed(str(e)) items = self.library.fetchItems(search_data[2]) + total_items = len(items) previous = None sort_edit = False for i, item in enumerate(items, 0): @@ -3571,7 +4025,7 @@ def sort_collection(self): if len(self.items) <= i or item.ratingKey != self.items[i].ratingKey: text = f"after {util.item_title(previous)}" if previous else "to the beginning" self.library.moveItem(self.obj, item, previous) - logger.info(f"Moving {util.item_title(item)} {text}") + logger.info(f"({i + 1}/{total_items}) Moving {util.item_title(item)} {text}") sort_edit = True previous = item except Failed: @@ -3660,7 +4114,7 @@ def exclude_admin_from_playlist(self): logger.info(f"Playlist: {self.name} not found on User {self.library.account.username}") def send_notifications(self, playlist=False): - if self.obj and self.details["changes_webhooks"] and \ + if self.obj is not None and self.details["changes_webhooks"] and \ (self.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0): self.library.item_reload(self.obj) try: diff --git a/modules/cache.py b/modules/cache.py index a0f58d1ab..b5e4d921b 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -218,19 +218,27 @@ def __init__(self, config_path, expiration): expiration_date TEXT)""" ) cursor.execute( - """CREATE TABLE IF NOT EXISTS tvdb_data4 ( - key INTEGER PRIMARY KEY, - tvdb_id INTEGER UNIQUE, - type TEXT, - title TEXT, - status TEXT, - summary TEXT, - poster_url TEXT, - background_url TEXT, - release_date TEXT, - genres TEXT, - expiration_date TEXT)""" + """CREATE TABLE IF NOT EXISTS tvdb_data5 ( + key INTEGER PRIMARY KEY, + tvdb_id INTEGER, + type TEXT, + title TEXT, + status TEXT, + summary TEXT, + poster_url TEXT, + background_url TEXT, + release_date TEXT, + genres TEXT, + -- neu: + networks TEXT, + production TEXT, + studio TEXT, + expiration_date TEXT, + UNIQUE(tvdb_id, type) + )""" ) + self._upgrade_tvdb_4_to_5(cursor) + cursor.execute( """CREATE TABLE IF NOT EXISTS tvdb_map ( key INTEGER PRIMARY KEY, @@ -331,6 +339,49 @@ def __init__(self, config_path, expiration): final_table = table_name if row["type"] == "poster" else f"{table_name}_backgrounds" self.update_image_map(row["rating_key"], final_table, row["location"], row["compare"], overlay=row["overlay"]) cursor.execute("DROP TABLE IF EXISTS image_map") + self.add_tables_for_emby() + + def add_tables_for_emby(self): + import sqlite3 + with sqlite3.connect(self.cache_path) as conn: + cursor = conn.cursor() + + # 1) Personen-Map (neu) + cursor.execute( + """CREATE TABLE IF NOT EXISTS tmdb_person_map ( + key INTEGER PRIMARY KEY, + tmdb_id INTEGER UNIQUE, + emby_id TEXT, + name TEXT, + alias TEXT, + meta_json TEXT, + expiration_date TEXT + )""" + ) + cursor.execute( + "CREATE INDEX IF NOT EXISTS idx_tmdb_person_map_tid ON tmdb_person_map(tmdb_id)" + ) + + # 2) Spalten in tmdb_movie_data nur hinzufügen, wenn sie fehlen + cursor.execute("PRAGMA table_info(tmdb_movie_data)") + columns = [row[1] for row in cursor.fetchall()] + + if "cast" not in columns: + try: + cursor.execute("ALTER TABLE tmdb_movie_data ADD COLUMN cast TEXT") + except sqlite3.OperationalError as e: + # falls doch schon vorhanden (Race / ältere Migration) + if "duplicate column name" not in str(e).lower(): + raise + + if "crew" not in columns: + try: + cursor.execute("ALTER TABLE tmdb_movie_data ADD COLUMN crew TEXT") + except sqlite3.OperationalError as e: + if "duplicate column name" not in str(e).lower(): + raise + + conn.commit() def query_guid_map(self, plex_guid): id_to_return = None @@ -620,6 +671,8 @@ def update_mal(self, expired, mal_id, mal, expiration): mal.rating, mal.score, mal.rank, mal.popularity, "|".join(mal.genres), mal.studio, expiration_date.strftime("%Y-%m-%d"), mal_id )) + import json + def query_tmdb_movie(self, tmdb_id, expiration): tmdb_dict = {} expired = None @@ -629,44 +682,87 @@ def query_tmdb_movie(self, tmdb_id, expiration): cursor.execute("SELECT * FROM tmdb_movie_data WHERE tmdb_id = ?", (tmdb_id,)) row = cursor.fetchone() if row: - tmdb_dict["title"] = row["title"] if row["title"] else "" - tmdb_dict["original_title"] = row["original_title"] if row["original_title"] else "" - tmdb_dict["studio"] = row["studio"] if row["studio"] else "" - tmdb_dict["overview"] = row["overview"] if row["overview"] else "" - tmdb_dict["tagline"] = row["tagline"] if row["tagline"] else "" - tmdb_dict["imdb_id"] = row["imdb_id"] if row["imdb_id"] else "" - tmdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] else "" - tmdb_dict["backdrop_url"] = row["backdrop_url"] if row["backdrop_url"] else "" - tmdb_dict["vote_count"] = row["vote_count"] if row["vote_count"] else 0 - tmdb_dict["vote_average"] = row["vote_average"] if row["vote_average"] else 0 - tmdb_dict["language_iso"] = row["language_iso"] if row["language_iso"] else None - tmdb_dict["language_name"] = row["language_name"] if row["language_name"] else None - tmdb_dict["genres"] = row["genres"] if row["genres"] else "" - tmdb_dict["keywords"] = row["keywords"] if row["keywords"] else "" - tmdb_dict["release_date"] = datetime.strptime(row["release_date"], "%Y-%m-%d") if row["release_date"] else None - tmdb_dict["collection_id"] = row["collection_id"] if row["collection_id"] else None - tmdb_dict["collection_name"] = row["collection_name"] if row["collection_name"] else None - datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") - time_between_insertion = datetime.now() - datetime_object - expired = time_between_insertion.days > expiration + tmdb_dict["title"] = row["title"] or "" + tmdb_dict["original_title"] = row["original_title"] or "" + tmdb_dict["studio"] = row["studio"] or "" + tmdb_dict["overview"] = row["overview"] or "" + tmdb_dict["tagline"] = row["tagline"] or "" + tmdb_dict["imdb_id"] = row["imdb_id"] or "" + tmdb_dict["poster_url"] = row["poster_url"] or "" + tmdb_dict["backdrop_url"] = row["backdrop_url"] or "" + tmdb_dict["vote_count"] = row["vote_count"] or 0 + tmdb_dict["vote_average"] = row["vote_average"] or 0 + tmdb_dict["language_iso"] = row["language_iso"] + tmdb_dict["language_name"] = row["language_name"] + # WICHTIG: als Pipe-String belassen (kompatibel zu _load .split) + tmdb_dict["genres"] = row["genres"] or "" + tmdb_dict["keywords"] = row["keywords"] or "" + tmdb_dict["release_date"] = datetime.strptime(row["release_date"], "%Y-%m-%d") if row[ + "release_date"] else None + tmdb_dict["collection_id"] = row["collection_id"] + tmdb_dict["collection_name"] = row["collection_name"] + # Cast/Crew als JSON speichern/laden + tmdb_dict["cast"] = json.loads(row["cast"]) if row["cast"] else [] + tmdb_dict["crew"] = json.loads(row["crew"]) if row["crew"] else [] + # Expiration + dt = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + expired = (datetime.now() - dt).days > expiration return tmdb_dict, expired def update_tmdb_movie(self, expired, obj, expiration): - expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + expiration_date = datetime.now() if expired is True else ( + datetime.now() - timedelta(days=random.randint(1, expiration))) with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: cursor.execute("INSERT OR IGNORE INTO tmdb_movie_data(tmdb_id) VALUES(?)", (obj.tmdb_id,)) - update_sql = "UPDATE tmdb_movie_data SET title = ?, original_title = ?, studio = ?, overview = ?, tagline = ?, imdb_id = ?, " \ - "poster_url = ?, backdrop_url = ?, vote_count = ?, vote_average = ?, language_iso = ?, " \ - "language_name = ?, genres = ?, keywords = ?, release_date = ?, collection_id = ?, " \ - "collection_name = ?, expiration_date = ? WHERE tmdb_id = ?" + update_sql = """ + UPDATE tmdb_movie_data SET + title = ?, original_title = ?, studio = ?, overview = ?, tagline = ?, imdb_id = ?, + poster_url = ?, backdrop_url = ?, vote_count = ?, vote_average = ?, language_iso = ?, + language_name = ?, genres = ?, keywords = ?, release_date = ?, collection_id = ?, + collection_name = ?, expiration_date = ?, cast = ?, crew = ? + WHERE tmdb_id = ? + """ + genres_str = "|".join(obj.genres or []) + keywords_str = "|".join(obj.keywords or []) + cast_json = json.dumps(obj.cast or []) + crew_json = json.dumps(obj.crew or []) + cursor.execute(update_sql, ( - obj.title, obj.original_title, obj.studio, obj.overview, obj.tagline, obj.imdb_id, obj.poster_url, obj.backdrop_url, - obj.vote_count, obj.vote_average, obj.language_iso, obj.language_name, "|".join(obj.genres), "|".join(obj.keywords), - obj.release_date.strftime("%Y-%m-%d") if obj.release_date else None, obj.collection_id, obj.collection_name, - expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id + obj.title, obj.original_title, obj.studio, obj.overview, obj.tagline, obj.imdb_id, + obj.poster_url, obj.backdrop_url, obj.vote_count, obj.vote_average, obj.language_iso, + obj.language_name, genres_str, keywords_str, + obj.release_date.strftime("%Y-%m-%d") if obj.release_date else None, + obj.collection_id, obj.collection_name, + expiration_date.strftime("%Y-%m-%d"), + cast_json, crew_json, + obj.tmdb_id )) + # --- Person-Map aus den bereits vorliegenden cast/crew-Daten vorbefüllen (tmdb_id + name) --- + try: + def _iter_people(lst): + for it in (obj.cast or [] if lst == "cast" else obj.crew or []): + if isinstance(it, dict): + tid = it.get("person_id") or it.get("id") + nm = it.get("name") + else: + tid = getattr(it, "person_id", None) or getattr(it, "id", None) + nm = getattr(it, "name", None) + if tid and nm and str(tid).isdigit(): + yield int(tid), str(nm) + + seen = set() + for tid, nm in list(_iter_people("cast")) + list(_iter_people("crew")): + if tid in seen: + continue + seen.add(tid) + try: + self.update_tmdb_person_map(False, tid, name=nm, expiration=self.expiration) + except Exception: + pass + except Exception: + pass def query_tmdb_show(self, tmdb_id, expiration): tmdb_dict = {} @@ -765,41 +861,73 @@ def update_tmdb_episode(self, expired, obj, expiration): expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id, obj.season_number, obj.episode_number )) + def _tvdb_type(self, is_movie: bool) -> str: + return "movie" if is_movie else "show" + + def query_tvdb(self, tvdb_id, is_movie, expiration): tvdb_dict = {} expired = None + media_type = self._tvdb_type(is_movie) with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM tvdb_data4 WHERE tvdb_id = ? and type = ?", (tvdb_id, "movie" if is_movie else "show")) + cursor.execute( + "SELECT * FROM tvdb_data5 WHERE tvdb_id = ? AND type = ?", + (tvdb_id, media_type) + ) row = cursor.fetchone() if row: tvdb_dict["tvdb_id"] = int(row["tvdb_id"]) if row["tvdb_id"] else 0 - tvdb_dict["type"] = row["type"] if row["type"] else "" - tvdb_dict["title"] = row["title"] if row["title"] else "" - tvdb_dict["status"] = row["status"] if row["status"] else "" - tvdb_dict["summary"] = row["summary"] if row["summary"] else "" - tvdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] else "" - tvdb_dict["background_url"] = row["background_url"] if row["background_url"] else "" - tvdb_dict["release_date"] = datetime.strptime(row["release_date"], "%Y-%m-%d") if row["release_date"] else None - tvdb_dict["genres"] = row["genres"] if row["genres"] else "" + tvdb_dict["type"] = row["type"] or "" + tvdb_dict["title"] = row["title"] or "" + tvdb_dict["status"] = row["status"] or "" + tvdb_dict["summary"] = row["summary"] or "" + tvdb_dict["poster_url"] = row["poster_url"] or "" + tvdb_dict["background_url"] = row["background_url"] or "" + tvdb_dict["release_date"] = datetime.strptime(row["release_date"], "%Y-%m-%d") if row[ + "release_date"] else None + tvdb_dict["genres"] = row["genres"] or "" + # neu: + tvdb_dict["networks"] = row["networks"] or "" + tvdb_dict["production"] = row["production"] or "" + tvdb_dict["studio"] = row["studio"] or "" + datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") - time_between_insertion = datetime.now() - datetime_object - expired = time_between_insertion.days > expiration + expired = (datetime.now() - datetime_object).days > expiration return tvdb_dict, expired def update_tvdb(self, expired, obj, expiration): - expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + media_type = self._tvdb_type(obj.is_movie) + expiration_date = datetime.now() if expired is True else ( + datetime.now() - timedelta(days=random.randint(1, expiration))) with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("INSERT OR IGNORE INTO tvdb_data4(tvdb_id, type) VALUES(?, ?)", (obj.tvdb_id, "movie" if obj.is_movie else "show")) - update_sql = "UPDATE tvdb_data4 SET title = ?, status = ?, summary = ?, poster_url = ?, background_url = ?, " \ - "release_date = ?, genres = ?, expiration_date = ? WHERE tvdb_id = ? AND type = ?" - tvdb_date = f"{str(obj.release_date.year).zfill(4)}-{str(obj.release_date.month).zfill(2)}-{str(obj.release_date.day).zfill(2)}" if obj.release_date else None + cursor.execute( + "INSERT OR IGNORE INTO tvdb_data5(tvdb_id, type) VALUES(?, ?)", + (obj.tvdb_id, media_type) + ) + update_sql = ( + "UPDATE tvdb_data5 SET title = ?, status = ?, summary = ?, poster_url = ?, background_url = ?, " + "release_date = ?, genres = ?, networks = ?, production = ?, studio = ?, expiration_date = ? " + "WHERE tvdb_id = ? AND type = ?" + ) + tvdb_date = ( + f"{obj.release_date.year:04d}-{obj.release_date.month:02d}-{obj.release_date.day:02d}" + if getattr(obj, 'release_date', None) else None + ) + # optional: falls die Attribute fehlen, leer schreiben + networks = getattr(obj, "networks", "") + production = getattr(obj, "production", "") + studio = getattr(obj, "studio", "") + cursor.execute(update_sql, ( - obj.title, obj.status, obj.summary, obj.poster_url, obj.background_url, tvdb_date, "|".join(obj.genres), - expiration_date.strftime("%Y-%m-%d"), obj.tvdb_id, "movie" if obj.is_movie else "show" + obj.title, obj.status, obj.summary, obj.poster_url, obj.background_url, + tvdb_date, "|".join(obj.genres), + networks, production, studio, + expiration_date.strftime("%Y-%m-%d"), + obj.tvdb_id, media_type )) def query_tvdb_map(self, tvdb_url, expiration): @@ -892,6 +1020,14 @@ def get_image_table_name(self, library): compare TEXT, location TEXT)""" ) + cursor.execute( + f"""CREATE TABLE IF NOT EXISTS {table_name}_logos ( + key INTEGER PRIMARY KEY, + rating_key TEXT UNIQUE, + overlay TEXT, + compare TEXT, + location TEXT)""" + ) cursor.execute( f"""CREATE TABLE IF NOT EXISTS {table_name}_overlays ( key INTEGER PRIMARY KEY, @@ -1128,3 +1264,165 @@ def update_testing(self, name, value1, value2, success): cursor.execute(f"INSERT OR IGNORE INTO testing(name) VALUES(?)", (name,)) sql = f"UPDATE testing SET value1 = ?, value2 = ?, success = ? WHERE name = ?" cursor.execute(sql, (value1, value2, success, name)) + + def _upgrade_tvdb_4_to_5(self, cursor): + """ + Migrates tvdb_data4 -> tvdb_data5 und vereinheitlicht das Schema: + - fügt networks, production, studio hinzu + - stellt UNIQUE(tvdb_id, type) sicher + - übernimmt ggf. vorhandene Daten aus einer alten tvdb_data5-Version + """ + + def _table_exists(name: str) -> bool: + cursor.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)) + return cursor.fetchone() is not None + + if not _table_exists("tvdb_data4"): + return + + # Zieltabelle als NEU bauen (finales Schema) + cursor.execute( + """CREATE TABLE IF NOT EXISTS tvdb_data5_new ( + key INTEGER PRIMARY KEY, + tvdb_id INTEGER, + type TEXT, + title TEXT, + status TEXT, + summary TEXT, + poster_url TEXT, + background_url TEXT, + release_date TEXT, + genres TEXT, + networks TEXT, + production TEXT, + studio TEXT, + expiration_date TEXT, + UNIQUE(tvdb_id, type) + )""" + ) + + # 1) Aus evtl. vorhandener "alter" tvdb_data5 übernehmen + if _table_exists("tvdb_data4"): + try: + # Falls alte tvdb_data5 die drei Spalten bereits hat + cursor.execute( + """INSERT OR IGNORE INTO tvdb_data5_new + (tvdb_id, type, title, status, summary, poster_url, background_url, + release_date, genres, networks, production, studio, expiration_date) + SELECT tvdb_id, type, title, status, summary, poster_url, background_url, + release_date, genres, networks, production, studio, expiration_date + FROM tvdb_data4""" + ) + except sqlite3.OperationalError: + # Alte tvdb_data5 ohne die drei Spalten + cursor.execute( + """INSERT OR IGNORE INTO tvdb_data5_new + (tvdb_id, type, title, status, summary, poster_url, background_url, + release_date, genres, expiration_date) + SELECT tvdb_id, type, title, status, summary, poster_url, background_url, + release_date, genres, expiration_date + FROM tvdb_data4""" + ) + + # 3) Alt durch Neu ersetzen (atomic im gleichen Connection-Kontext) + if _table_exists("tvdb_data5"): + cursor.execute("DROP TABLE tvdb_data5") + cursor.execute("ALTER TABLE tvdb_data5_new RENAME TO tvdb_data5") + + # 4) tvdb_data4 nach erfolgreicher Migration entfernen + if _table_exists("tvdb_data4"): + cursor.execute("DROP TABLE tvdb_data4") + + try: + logger.info("tvdb_data: Upgrade auf v5 abgeschlossen.") + except Exception: + pass + + def query_tmdb_person_map_bulk(self, tmdb_ids, expiration): + mapping, missing_ids, expired_ids = {}, set(tmdb_ids or []), set() + if not tmdb_ids: + return mapping, missing_ids, expired_ids + # if True: + # return mapping, missing_ids, expired_ids + + + qmarks = ",".join("?" * len(tmdb_ids)) + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute(f"SELECT * FROM tmdb_person_map WHERE tmdb_id IN ({qmarks})", tuple(tmdb_ids)) + for row in cursor.fetchall(): + tid = int(row["tmdb_id"]) + try: + meta = json.loads(row["meta_json"]) if row["meta_json"] else {} + except Exception: + meta = {} + mapping[tid] = { + "emby_id": row["emby_id"], + "name": row["name"], + "alias": row["alias"], + "meta": meta + } + missing_ids.discard(tid) + if row["expiration_date"]: + dt = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + if (datetime.now() - dt).days > expiration: + expired_ids.add(tid) + return mapping, missing_ids, expired_ids + + + def update_tmdb_person_map(self, expired, tmdb_id, emby_id=None, name=None, alias=None, meta_patch=None, expiration=None): + if expiration is None: + expiration = self.expiration if hasattr(self, "expiration") else 30 + expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("INSERT OR IGNORE INTO tmdb_person_map(tmdb_id) VALUES(?)", (tmdb_id,)) + cursor.execute("SELECT emby_id, name, alias, meta_json FROM tmdb_person_map WHERE tmdb_id = ?", (tmdb_id,)) + row = cursor.fetchone() + cur_emby = row["emby_id"] if row else None + cur_name = row["name"] if row else None + cur_alias = row["alias"] if row else None + try: + cur_meta = json.loads(row["meta_json"]) if (row and row["meta_json"]) else {} + except Exception: + cur_meta = {} + + new_emby = emby_id if emby_id is not None else cur_emby + new_name = name if name is not None else cur_name + new_alias = alias if alias is not None else cur_alias + if meta_patch: + try: cur_meta.update(meta_patch) + except Exception: pass + + cursor.execute( + "UPDATE tmdb_person_map SET emby_id = ?, name = ?, alias = ?, meta_json = ?, expiration_date = ? WHERE tmdb_id = ?", + (new_emby, new_name, new_alias, json.dumps(cur_meta, ensure_ascii=False) if cur_meta else None, + expiration_date.strftime("%Y-%m-%d"), tmdb_id) + ) + + def query_false_friend_names(self) -> set[str]: + names = set() + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("CREATE TABLE IF NOT EXISTS false_friend_names (name TEXT PRIMARY KEY)") + cursor.execute("SELECT name FROM false_friend_names") + for row in cursor.fetchall(): + nm = (row["name"] or "").strip() + if nm: + names.add(nm.casefold()) + return names + + def add_false_friend_name(self, name: str) -> None: + nm = (name or "").strip() + logger.info(f"Added 'false friend' {name} to alias list.") + if not nm: + return + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("CREATE TABLE IF NOT EXISTS false_friend_names (name TEXT PRIMARY KEY)") + cursor.execute("INSERT OR IGNORE INTO false_friend_names(name) VALUES (?)", (nm.casefold(),)) diff --git a/modules/config.py b/modules/config.py index 23c3e68f3..b69e32c69 100644 --- a/modules/config.py +++ b/modules/config.py @@ -5,6 +5,7 @@ from modules.anilist import AniList from modules.cache import Cache from modules.convert import Convert +from modules.emby import Emby from modules.ergast import Ergast from modules.icheckmovies import ICheckMovies from modules.imdb import IMDb @@ -100,8 +101,8 @@ "lock": "Lock Rating", "unlock": "Unlock Rating", "remove": "Remove and Lock Rating", "reset": "Remove and Unlock Rating", "plex_tmdb": "Use TMDB Rating through Plex", "plex_imdb": "Use IMDB Rating through Plex", - "tmdb": "Use TMDb Rating", - "imdb": "Use IMDb Rating", + "tmdb": "Use TMDb Rating", + "imdb": "Use IMDb Rating", "trakt": "Use Trakt Rating" } mass_rating_options = { @@ -138,7 +139,7 @@ } reset_overlay_options = {"tmdb": "Reset to TMDb poster", "plex": "Reset to Plex Poster"} library_operations = { - "assets_for_all": "bool", "split_duplicates": "bool", "update_blank_track_titles": "bool", "remove_title_parentheses": "bool", + "assets_for_all": "bool", "assets_for_all_collections": "bool", "split_duplicates": "bool", "update_blank_track_titles": "bool", "remove_title_parentheses": "bool", "radarr_add_all_existing": "bool", "radarr_remove_by_tag": "str", "sonarr_add_all_existing": "bool", "sonarr_remove_by_tag": "str", "mass_content_rating_update": mass_content_options, "mass_collection_content_rating_update": "dict", "mass_genre_update": mass_genre_options, "mass_studio_update": mass_studio_options, @@ -511,7 +512,8 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio "custom_repo": check_for_attribute(self.data, "custom_repo", parent="settings", default_is_none=True), "overlay_artwork_filetype": check_for_attribute(self.data, "overlay_artwork_filetype", parent="settings", test_list=filetype_list, translations={"webp": "webp_lossy"}, default="webp_lossy"), "overlay_artwork_quality": check_for_attribute(self.data, "overlay_artwork_quality", parent="settings", var_type="int", default=90, int_min=1, int_max=100), - "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False, save=False, do_print=False) + "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False, save=False, do_print=False), + "assets_for_all_collections": check_for_attribute(self.data, "assets_for_all_collections", parent="settings", var_type="bool", default=False, save=False, do_print=False) } self.custom_repo = None if self.general["custom_repo"]: @@ -704,6 +706,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio "client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True), "client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True), "pin": check_for_attribute(self.data, "pin", parent="trakt", default_is_none=True), + "force_refresh": check_for_attribute(self.data, "force_refresh", parent="trakt", default_is_none=True), "config_path": self.config_path, "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None }) @@ -816,6 +819,15 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio "verify_ssl": check_for_attribute(self.data, "verify_ssl", parent="plex", var_type="bool", default_is_none=True), "db_cache": check_for_attribute(self.data, "db_cache", parent="plex", var_type="int", default_is_none=True) } + self.general["emby"] = { + "url": check_for_attribute(self.data, "url", parent="emby", var_type="url", default_is_none=True), + "api_key": check_for_attribute(self.data, "api_key", parent="emby", default_is_none=True), + "user_id": check_for_attribute(self.data, "user_id", parent="emby", default_is_none=True), + "overlay_destination_folder": check_for_attribute(self.data, "overlay_destination_folder", parent="emby", default_is_none=True), + "timeout": check_for_attribute(self.data, "timeout", parent="emby", var_type="int", default=60), + "verify_ssl": check_for_attribute(self.data, "verify_ssl", parent="emby", var_type="bool", default_is_none=True), + "db_cache": check_for_attribute(self.data, "db_cache", parent="emby", var_type="int", default_is_none=True) + } for attr in ["clean_bundles", "empty_trash", "optimize"]: try: self.general["plex"][attr] = check_for_attribute(self.data, attr, parent="plex", var_type="bool", default=False, throw=True) @@ -1216,6 +1228,15 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio "verify_ssl": check_for_attribute(lib, "verify_ssl", parent="plex", var_type="bool", default=self.general["plex"]["verify_ssl"], default_is_none=True, save=False), "db_cache": check_for_attribute(lib, "db_cache", parent="plex", var_type="int", default=self.general["plex"]["db_cache"], default_is_none=True, save=False) } + params["emby"] = { + "url": check_for_attribute(lib, "url", parent="emby", var_type="url", default=self.general["emby"]["url"], req_default=True, save=False), + "api_key": check_for_attribute(lib, "api_key", parent="emby", default=self.general["emby"]["api_key"], req_default=True, save=False), + "user_id": check_for_attribute(lib, "user_id", parent="emby", default=self.general["emby"]["user_id"], req_default=True, save=False), + "overlay_destination_folder": check_for_attribute(lib, "overlay_destination_folder", parent="emby", default=self.general["emby"]["overlay_destination_folder"], req_default=True, save=False), + "timeout": check_for_attribute(lib, "timeout", parent="emby", var_type="int", default=self.general["emby"]["timeout"], save=False), + "verify_ssl": check_for_attribute(lib, "verify_ssl", parent="emby", var_type="bool", default=self.general["emby"]["verify_ssl"], default_is_none=True, save=False), + "db_cache": check_for_attribute(lib, "db_cache", parent="emby", var_type="int", default=self.general["emby"]["db_cache"], default_is_none=True, save=False) + } for attr in ["clean_bundles", "empty_trash", "optimize"]: try: params["plex"][attr] = check_for_attribute(lib, attr, parent="plex", var_type="bool", save=False, throw=True) @@ -1236,18 +1257,44 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio if params["plex"]["token"].lower() == "env": params["plex"]["token"] = self.env_plex_token library = Plex(self, params) + # ToDo - Develop, document + emby_library = Emby(self, params) + if self.data["settings"] and 'server_type' in self.data["settings"]: + the_type = self.data.get("settings").get("server_type") + match the_type: + case "emby": + # library = Emby(self, params) + pass + case "jellyfin": + # library = Jellyfin(self, params) + pass + case default: + # library = Plex(self, params) + pass + + + pass + logger.info("") logger.info(f"{display_name} Library Connection Successful") logger.info("") logger.separator("Scanning Files", space=False, border=False) library.scan_files(self.operations_only, self.overlays_only, self.collection_only, self.metadata_only) + # test + if False: + emby_library.scan_files(self.operations_only, self.overlays_only, self.collection_only, self.metadata_only) + if not emby_library.collection_files and not emby_library.metadata_files and not emby_library.overlay_files and not emby_library.library_operation and not emby_library.images_files and not self.playlist_files: + raise Failed( + "Config Error: No valid collection file, metadata file, overlay file, image file, playlist file, or library operations found") + # test end + if not library.collection_files and not library.metadata_files and not library.overlay_files and not library.library_operation and not library.images_files and not self.playlist_files: raise Failed("Config Error: No valid collection file, metadata file, overlay file, image file, playlist file, or library operations found") except Failed as e: logger.stacktrace() logger.error(e) logger.info("") - logger.info(f"{display_name} Library Connection Failed") + logger.info(f"Plex {display_name} Library Connection Failed") continue if self.general["radarr"]["url"] or (lib and "radarr" in lib): diff --git a/modules/convert.py b/modules/convert.py index fcd02b053..527e901d0 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -216,7 +216,7 @@ def tvdb_to_tmdb(self, tvdb_id, fail=False): except Failed: pass if fail: - raise Failed(f"Convert Warning: No TMDb ID Found for TVDb ID: {tvdb_id}") + raise Failed(f"Convert Warning: No TMDb ID Found for TVDb ID: https://thetvdb.com/dereferrer/series/{tvdb_id}") else: return None @@ -280,15 +280,19 @@ def scan_guid(self, guid_str): guid = urlparse(guid_str) return guid.scheme.split(".")[-1], guid.netloc - def get_id(self, item, library): + def get_id(self, item, library, mydata): expired = None tmdb_id = [] tvdb_id = [] imdb_id = [] anidb_id = None item_type, check_id = self.scan_guid(item.guid) + + item_type = "emby" + media_id_type, cache_id, imdb_check, expired = self.ids_from_cache(item.ratingKey, item.guid, item_type, check_id, library) if (cache_id or imdb_check) and expired is False: + # print("cached") return media_id_type, cache_id, imdb_check try: if item_type == "plex": @@ -308,6 +312,27 @@ def get_id(self, item, library): if not tvdb_id and not imdb_id and not tmdb_id: library.query(item.refresh) raise Failed("Refresh Metadata") + elif item_type == "emby": + db_imdb = mydata[0] + db_tvdb = mydata[1] + db_tmdb = mydata[2] + # media_type = mydata[3] + + if db_tvdb is not None: + try: + int(db_tvdb) + except : + db_tvdb = db_tvdb.split("/")[1] + if db_tvdb is not None: + try: + tvdb_id.append(int(db_tvdb)) + except: + print(f"error tvdb_id: {item.title} - {item.ratingKey} - {db_tvdb}") + if db_imdb: + imdb_id.append(db_imdb) + if db_tmdb: + tmdb_id.append(int(db_tmdb)) + elif item_type == "imdb": imdb_id.append(check_id) elif item_type == "thetvdb": tvdb_id.append(int(check_id)) elif item_type == "themoviedb": tmdb_id.append(int(check_id)) @@ -385,8 +410,8 @@ def update_cache(cache_ids, id_type, imdb_in, guid_type): if self.cache: cache_ids = ",".join([str(c) for c in cache_ids]) imdb_in = ",".join([str(i) for i in imdb_in]) if imdb_in else None - ids = f"{item.guid:<46} | {id_type} ID: {cache_ids:<7} | IMDb ID: {str(imdb_in):<10}" - logger.info(f" Cache | {'^' if expired else '+'} | {ids} | {item.title}") + ids = f"{item.ratingKey:<13} | {id_type} ID: {cache_ids:<7} | IMDb ID: {str(imdb_in):<10}" + logger.info(f" Cache | {'^' if expired else '+'} | {ids} | {item.title:<20}") self.cache.update_guid_map(item.guid, cache_ids, imdb_in, expired, guid_type) if (tmdb_id or imdb_id) and library.is_movie: @@ -402,11 +427,11 @@ def update_cache(cache_ids, id_type, imdb_in, guid_type): logger.debug(f"TMDb: {tmdb_id}, IMDb: {imdb_id}, TVDb: {tvdb_id}") raise Failed(f"No ID to convert") except Failed as e: - logger.info(f'Mapping Error | {item.guid:<46} | {e} for "{item.title}"') + logger.info(f'Mapping Error | {item.ratingKey:<13} | {e} for "{item.title:<20}"') except NonExisting as e: if not library.is_other: - logger.info(f'Mapping Error | {item.guid:<46} | {e} for "{item.title}"') + logger.info(f'Mapping Error | {item.ratingKey:<13} | {e} for "{item.title:<20}"') except BadRequest: logger.stacktrace() - logger.info(f'Mapping Error | {item.guid:<46} | Bad Request for "{item.title}"') + logger.info(f'Mapping Error | {item.ratingKey:<13} | Bad Request for "{item.title:<20}"') return None, None, None diff --git a/modules/emby.py b/modules/emby.py new file mode 100644 index 000000000..b3b7b09e9 --- /dev/null +++ b/modules/emby.py @@ -0,0 +1,753 @@ +import os +import re +import time +from datetime import datetime, timedelta +# from importlib.metadata import pass_none +from urllib.parse import unquote +from xml.etree.ElementTree import ParseError + +import requests +from PIL import Image +from plexapi import utils +from plexapi.audio import Artist, Track, Album +from plexapi.collection import Collection +from plexapi.exceptions import BadRequest, NotFound, Unauthorized +from plexapi.library import Role, FilterChoice +from plexapi.playlist import Playlist +from plexapi.video import Movie, Show, Season, Episode +from requests.exceptions import ConnectionError, ConnectTimeout +from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type + +from modules import builder, util +from modules.emby_server import EmbyServer, FilterChoiceEmby +from modules.library import Library +from modules.logs import WARNING +from modules.poster import ImageData +from modules.request import parse_qs, quote_plus, urlparse +from modules.util import Failed + +logger = util.logger + +builders = ["plex_all", "plex_watchlist", "plex_pilots", "plex_collectionless", "plex_search"] +library_types = ["movie", "show", "artist"] +search_translation = { + "episode_actor": "episode.actor", + "episode_title": "episode.title", + "network": "show.network", + "edition": "editionTitle", + "critic_rating": "rating", + "audience_rating": "audienceRating", + "episode_critic_rating": "episode.rating", + "episode_audience_rating": "episode.audienceRating", + "user_rating": "userRating", + "episode_user_rating": "episode.userRating", + "content_rating": "contentRating", + "episode_year": "episode.year", + "release": "originallyAvailableAt", + "show_unmatched": "show.unmatched", + "episode_unmatched": "episode.unmatched", + "episode_duplicate": "episode.duplicate", + "added": "addedAt", + "episode_added": "episode.addedAt", + "episode_air_date": "episode.originallyAvailableAt", + "plays": "viewCount", + "episode_plays": "episode.viewCount", + "last_played": "lastViewedAt", + "episode_last_played": "episode.lastViewedAt", + "unplayed": "unwatched", + "episode_unplayed": "episode.unwatched", + "dovi": "dovi", + "subtitle_language": "subtitleLanguage", + "audio_language": "audioLanguage", + "progress": "inProgress", + "episode_progress": "episode.inProgress", + "unplayed_episodes": "show.unwatchedLeaves", + "season_collection": "season.collection", + "episode_collection": "episode.collection", + "season_label": "season.label", + "episode_label": "episode.label", + "artist_title": "artist.title", + "artist_user_rating": "artist.userRating", + "artist_genre": "artist.genre", + "artist_collection": "artist.collection", + "artist_country": "artist.country", + "artist_mood": "artist.mood", + "artist_style": "artist.style", + "artist_added": "artist.addedAt", + "artist_last_played": "artist.lastViewedAt", + "artist_unmatched": "artist.unmatched", + "artist_label": "artist.label", + "album_title": "album.title", + "album_year": "album.year", + "album_decade": "album.decade", + "album_genre": "album.genre", + "album_plays": "album.viewCount", + "album_last_played": "album.lastViewedAt", + "album_user_rating": "album.userRating", + "album_critic_rating": "album.rating", + "album_record_label": "album.studio", + "album_mood": "album.mood", + "album_style": "album.style", + "album_format": "album.format", + "album_type": "album.subformat", + "album_collection": "album.collection", + "album_added": "album.addedAt", + "album_released": "album.originallyAvailableAt", + "album_unmatched": "album.unmatched", + "album_source": "album.source", + "album_label": "album.label", + "track_mood": "track.mood", + "track_title": "track.title", + "track_plays": "track.viewCount", + "track_last_played": "track.lastViewedAt", + "track_skips": "track.skipCount", + "track_last_skipped": "track.lastSkippedAt", + "track_user_rating": "track.userRating", + "track_last_rated": "track.lastRatedAt", + "track_added": "track.addedAt", + "track_trash": "track.trash", + "track_source": "track.source", + "track_label": "track.label" +} +show_translation = { + "title": "show.title", + "country": "show.country", + "studio": "show.studio", + "rating": "show.rating", + "audienceRating": "show.audienceRating", + "userRating": "show.userRating", + "contentRating": "show.contentRating", + "year": "show.year", + "originallyAvailableAt": "show.originallyAvailableAt", + "unmatched": "show.unmatched", + "genre": "show.genre", + "collection": "show.collection", + "actor": "show.actor", + "addedAt": "show.addedAt", + "viewCount": "show.viewCount", + "lastViewedAt": "show.lastViewedAt", + "resolution": "episode.resolution", + "hdr": "episode.hdr", + "subtitleLanguage": "episode.subtitleLanguage", + "audioLanguage": "episode.audioLanguage", + "trash": "episode.trash", + "label": "show.label", +} +get_tags_translation = {"episode.actor": "actor"} +modifier_translation = { + "": "", ".not": "!", ".is": "%3D", ".isnot": "!%3D", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C", + ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": "", ".rated": "" +} +attribute_translation = { + "aspect": "aspectRatio", + "channels": "audioChannels", + "audio_codec": "audioCodec", + "audio_profile ": "audioProfile", + "video_codec": "videoCodec", + "video_profile": "videoProfile", + "resolution": "videoResolution", + "record_label": "studio", + "similar_artist": "similar", + "actor": "actors", + "audience_rating": "audienceRating", + "collection": "collections", + "content_rating": "contentRating", + "country": "countries", + "critic_rating": "rating", + "director": "directors", + "genre": "genres", + "label": "labels", + "producer": "producers", + "composer": "composers", + "release": "originallyAvailableAt", + "originally_available": "originallyAvailableAt", + "added": "addedAt", + "last_played": "lastViewedAt", + "plays": "viewCount", + "user_rating": "userRating", + "writer": "writers", + "mood": "moods", + "style": "styles", + "episode_number": "episodeNumber", + "season_number": "seasonNumber", + "original_title": "originalTitle", + "edition": "editionTitle", + "runtime": "duration", + "season_title": "parentTitle", + "episode_count": "leafCount", + "versions": "media" +} +method_alias = { + "actors": "actor", "role": "actor", "roles": "actor", + "show_actor": "actor", "show_actors": "actor", "show_role": "actor", "show_roles": "actor", + "collections": "collection", "plex_collection": "collection", + "show_collections": "collection", "show_collection": "collection", + "content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating", + "countries": "country", + "decades": "decade", + "directors": "director", + "genres": "genre", + "labels": "label", + "collection_minimum": "minimum_items", + "playlist_minimum": "minimum_items", + "save_missing": "save_report", + "rating": "critic_rating", + "show_user_rating": "user_rating", + "video_resolution": "resolution", + "tmdb_trending": "tmdb_trending_daily", + "play": "plays", "show_plays": "plays", "show_play": "plays", "episode_play": "episode_plays", + "originally_available": "release", "episode_originally_available": "episode_air_date", + "episode_release": "episode_air_date", "episode_released": "episode_air_date", + "show_originally_available": "release", "show_release": "release", "show_air_date": "release", + "released": "release", "show_released": "release", "max_age": "release", + "studios": "studio", + "networks": "network", + "producers": "producer", + "composers": "composer", + "writers": "writer", + "years": "year", "show_year": "year", "show_years": "year", + "show_title": "title", "filter": "filters", + "seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score", + "minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent", + "anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search", + "mal_producer": "mal_studio", "mal_licensor": "mal_studio", + "trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly", + "collection_changes_webhooks": "changes_webhooks", + "radarr_add": "radarr_add_missing", "sonarr_add": "sonarr_add_missing", + "trakt_recommended_personal": "trakt_recommendations", + "collection_level": "builder_level", "overlay_level": "builder_level", +} +modifier_alias = {".greater": ".gt", ".less": ".lt"} +date_sub_mods = {"s": "Seconds", "m": "Minutes", "h": "Hours", "d": "Days", "w": "Weeks", "o": "Months", "y": "Years"} +album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2} +episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1} +keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30} +delete_episodes_options = {"never": 0, "day": 1, "week": 7, "month": 30, "refresh": 100} +season_display_options = {"default": -1, "show": 0, "hide": 1} +episode_ordering_options = {"default": None, "tmdb_aired": "tmdbAiring", "tvdb_aired": "tvdbAiring", "tvdb_dvd": "tvdbDvd", "tvdb_absolute": "tvdbAbsolute"} +plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", + "es-ES", "es-MX", "et-EE", "fa-IR", "fi-FI", "fr-CA", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID", + "it-IT", "ja-JP", "ko-KR", "lt-LT", "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", + "ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-HK", "zh-TW"] +metadata_language_options = {lang.lower(): lang for lang in plex_languages} +metadata_language_options["default"] = None +use_original_title_options = {"default": -1, "no": 0, "yes": 1} +credits_detection_options = {"default": -1, "disabled": 0} +audio_language_options = {lang.lower(): lang for lang in plex_languages} +audio_language_options["en"] = "en" +subtitle_language_options = {lang.lower(): lang for lang in plex_languages} +subtitle_language_options["en"] = "en" +subtitle_mode_options = {"default": -1, "manual": 0, "foreign": 1, "always": 2} +collection_order_options = ["release", "alpha", "custom"] +collection_filtering_options = ["user", "admin"] +collection_mode_options = { + "default": "default", "hide": "hide", + "hide_items": "hideItems", "hideitems": "hideItems", + "show_items": "showItems", "showitems": "showItems" +} +builder_level_show_options = ["episode", "season"] +builder_level_music_options = ["album", "track"] +builder_level_options = builder_level_show_options + builder_level_music_options +collection_mode_keys = {-1: "default", 0: "hide", 1: "hideItems", 2: "showItems"} +collection_order_keys = {0: "release", 1: "alpha", 2: "custom"} +item_advance_keys = { + "item_album_sorting": ("albumSort", album_sorting_options), + "item_episode_sorting": ("episodeSort", episode_sorting_options), + "item_keep_episodes": ("autoDeletionItemPolicyUnwatchedLibrary", keep_episodes_options), + "item_delete_episodes": ("autoDeletionItemPolicyWatchedLibrary", delete_episodes_options), + "item_season_display": ("flattenSeasons", season_display_options), + "item_episode_ordering": ("showOrdering", episode_ordering_options), + "item_metadata_language": ("languageOverride", metadata_language_options), + "item_use_original_title": ("useOriginalTitle", use_original_title_options), + "item_credits_detection": ("enableCreditsMarkerGeneration", credits_detection_options), + "item_audio_language": ("audioLanguage", audio_language_options), + "item_subtitle_language": ("subtitleLanguage", subtitle_language_options), + "item_subtitle_mode": ("subtitleMode", subtitle_mode_options) +} +new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"] +and_searches = [ + "title.and", "studio.and", "actor.and", "audio_language.and", "collection.and", + "content_rating.and", "country.and", "director.and", "genre.and", "label.and", + "network.and", "producer.and", "composer.and", "subtitle_language.and", "writer.and" +] +or_searches = [ + "title", "studio", "actor", "audio_language", "collection", "content_rating", + "country", "director", "genre", "label", "network", "producer", "composer", "subtitle_language", + "writer", "decade", "resolution", "year", "episode_title", "episode_year" +] +movie_only_searches = [ + "director", "director.not", "producer", "producer.not", "composer", "composer.not", "writer", "writer.not", + "decade", "duplicate", "unplayed", "progress", + "duration.gt", "duration.gte", "duration.lt", "duration.lte" + "edition", "edition.not", "edition.is", "edition.isnot", "edition.begins", "edition.ends" +] +show_only_searches = [ + "network", "network.not", + "season_collection", "season_collection.not", + "episode_collection", "episode_collection.not", + "season_label", "season_label.not", + "episode_label", "episode_label.not", + "episode_title", "episode_title.not", "episode_title.is", "episode_title.isnot", "episode_title.begins", "episode_title.ends", + "episode_added", "episode_added.not", "episode_added.before", "episode_added.after", + "episode_air_date", "episode_air_date.not", + "episode_air_date.before", "episode_air_date.after", + "episode_last_played", "episode_last_played.not", "episode_last_played.before", "episode_last_played.after", + "episode_plays.gt", "episode_plays.gte", "episode_plays.lt", "episode_plays.lte", + "episode_user_rating.gt", "episode_user_rating.gte", "episode_user_rating.lt", "episode_user_rating.lte", "episode_user_rating.rated", + "episode_critic_rating.gt", "episode_critic_rating.gte", "episode_critic_rating.lt", "episode_critic_rating.lte", "episode_critic_rating.rated", + "episode_audience_rating.gt", "episode_audience_rating.gte", "episode_audience_rating.lt", "episode_audience_rating.lte", "episode_audience_rating.rated", + "episode_year", "episode_year.not", "episode_year.gt", "episode_year.gte", "episode_year.lt", "episode_year.lte", + "unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched", "show_unmatched", +] +string_attributes = ["title", "studio", "edition", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"] +string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends"] +boolean_attributes = [ + "dovi", "hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", "unplayed_episodes", "episode_unplayed", + "episode_duplicate", "episode_progress", "episode_unmatched", "show_unmatched", "artist_unmatched", "album_unmatched", "track_trash" +] +tmdb_attributes = ["actor", "director", "producer", "composer", "writer"] +date_attributes = [ + "added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played", + "artist_added", "artist_last_played", "album_last_played", + "album_added", "album_released", "track_last_played", "track_last_skipped", "track_last_rated", "track_added" +] +date_modifiers = ["", ".not", ".before", ".after"] +year_attributes = ["decade", "year", "episode_year", "album_year", "album_decade"] +number_attributes = ["plays", "episode_plays", "album_plays", "track_plays", "track_skips"] + year_attributes +number_modifiers = [".gt", ".gte", ".lt", ".lte"] +float_attributes = [ + "user_rating", "episode_user_rating", "critic_rating", "episode_critic_rating", "audience_rating", "episode_audience_rating", + "duration", "artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating" +] +float_modifiers = number_modifiers + [".rated"] +search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"} +tag_attributes = [ + "actor", "episode_actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "season_label", "episode_label", "network", + "producer", "composer", "resolution", "studio", "subtitle_language", "writer", "season_collection", "episode_collection", "edition", + "artist_genre", "artist_collection", "artist_country", "artist_mood", "artist_label", "artist_style", "album_genre", "album_mood", + "album_style", "album_format", "album_type", "album_collection", "album_source", "album_label", "track_mood", "track_source", "track_label" +] +tag_modifiers = ["", ".not", ".regex"] +no_not_mods = ["resolution", "decade", "album_decade"] +searches = boolean_attributes + \ + [f"{f}{m}" for f in string_attributes for m in string_modifiers] + \ + [f"{f}{m}" for f in tag_attributes + year_attributes for m in tag_modifiers if f not in no_not_mods or m != ".not"] + \ + [f"{f}{m}" for f in date_attributes for m in date_modifiers] + \ + [f"{f}{m}" for f in number_attributes for m in number_modifiers if f not in no_not_mods] + \ + [f"{f}{m}" for f in float_attributes for m in float_modifiers if f != "duration" or m != ".rated"] +music_searches = [a for a in searches if a.startswith(("artist", "album", "track"))] +movie_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "year.asc": "year", "year.desc": "year%3Adesc", + "originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc", + "release.asc": "originallyAvailableAt", "release.desc": "originallyAvailableAt%3Adesc", + "critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc", + "audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "content_rating.asc": "contentRating", "content_rating.desc": "contentRating%3Adesc", + "duration.asc": "duration", "duration.desc": "duration%3Adesc", + "progress.asc": "viewOffset", "progress.desc": "viewOffset%3Adesc", + "plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc", + "resolution.asc": "mediaHeight", "resolution.desc": "mediaHeight%3Adesc", + "bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc", + "random": "random" +} +show_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "year.asc": "year", "year.desc": "year%3Adesc", + "originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc", + "episode_originally_available.asc": "episode.originallyAvailableAt", "episode_originally_available.desc": "episode.originallyAvailableAt%3Adesc", + "release.asc": "originallyAvailableAt", "release.desc": "originallyAvailableAt%3Adesc", + "episode_release.asc": "episode.originallyAvailableAt", "episode_release.desc": "episode.originallyAvailableAt%3Adesc", + "critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc", + "audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "content_rating.asc": "contentRating", "content_rating.desc": "contentRating%3Adesc", + "unplayed.asc": "unviewedLeafCount", "unplayed.desc": "unviewedLeafCount%3Adesc", + "episode_added.asc": "episode.addedAt", "episode_added.desc": "episode.addedAt%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc", + "random": "random" +} +season_sorts = { + "season.asc": "season.index%2Cseason.titleSort", "season.desc": "season.index%3Adesc%2Cseason.titleSort", + "show.asc": "show.titleSort%2Cindex", "show.desc": "show.titleSort%3Adesc%2Cindex", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "random": "random" +} +episode_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "show.asc": "show.titleSort%2Cseason.index%3AnullsLast%2Cepisode.index%3AnullsLast%2Cepisode.originallyAvailableAt%3AnullsLast%2Cepisode.titleSort%2Cepisode.id", + "show.desc": "show.titleSort%3Adesc%2Cseason.index%3AnullsLast%2Cepisode.index%3AnullsLast%2Cepisode.originallyAvailableAt%3AnullsLast%2Cepisode.titleSort%2Cepisode.id", + "year.asc": "year", "year.desc": "year%3Adesc", + "originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc", + "episode_originally_available.asc": "episode.originallyAvailableAt", "episode_originally_available.desc": "episode.originallyAvailableAt%3Adesc", + "release.asc": "originallyAvailableAt", "release.desc": "originallyAvailableAt%3Adesc", + "episode_release.asc": "episode.originallyAvailableAt", "episode_release.desc": "episode.originallyAvailableAt%3Adesc", + "critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc", + "audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "duration.asc": "duration", "duration.desc": "duration%3Adesc", + "progress.asc": "viewOffset", "progress.desc": "viewOffset%3Adesc", + "plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc", + "resolution.asc": "mediaHeight", "resolution.desc": "mediaHeight%3Adesc", + "bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc", + "random": "random" +} +artist_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc", + "plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc", + "random": "random" +} +album_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "album_artist.asc": "artist.titleSort%2Calbum.titleSort%2Calbum.index%2Calbum.id%2Calbum.originallyAvailableAt", + "album_artist.desc": "artist.titleSort%3Adesc%2Calbum.titleSort%2Calbum.index%2Calbum.id%2Calbum.originallyAvailableAt", + "year.asc": "year", "year.desc": "year%3Adesc", + "originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc", + "release.asc": "originallyAvailableAt", "release.desc": "originallyAvailableAt%3Adesc", + "critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc", + "plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc", + "random": "random" +} +track_sorts = { + "title.asc": "titleSort", "title.desc": "titleSort%3Adesc", + "album_artist.asc": "artist.titleSort%2Calbum.titleSort%2Calbum.year%2Ctrack.absoluteIndex%2Ctrack.index%2Ctrack.titleSort%2Ctrack.id", + "album_artist.desc": "artist.titleSort%3Adesc%2Calbum.titleSort%2Calbum.year%2Ctrack.absoluteIndex%2Ctrack.index%2Ctrack.titleSort%2Ctrack.id", + "artist.asc": "originalTitle", "artist.desc": "originalTitle%3Adesc", + "album.asc": "album.titleSort", "album.desc": "album.titleSort%3Adesc", + "user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc", + "duration.asc": "duration", "duration.desc": "duration%3Adesc", + "plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc", + "added.asc": "addedAt", "added.desc": "addedAt%3Adesc", + "played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc", + "rated.asc": "lastRatedAt", "rated.desc": "lastRatedAt%3Adesc", + "popularity.asc": "ratingCount", "popularity.desc": "ratingCount%3Adesc", + "bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc", + "random": "random" +} +sort_types = { + "movie": ("title.asc", 1, movie_sorts), + "show": ("title.asc", 2, show_sorts), + "season": ("season.asc", 3, season_sorts), + "episode": ("title.asc", 4, episode_sorts), + "artist": ("title.asc", 8, artist_sorts), + "album": ("title.asc", 9, album_sorts), + "track": ("title.asc", 10, track_sorts) +} +watchlist_sorts = { + "added.asc": "watchlistedAt:asc", "added.desc": "watchlistedAt:desc", + "title.asc": "titleSort:asc", "title.desc": "titleSort:desc", + "release.asc": "originallyAvailableAt:asc", "release.desc": "originallyAvailableAt:desc", + "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", +} + +MAX_IMAGE_SIZE = 10480000 # a little less than 10MB + +class Emby(Library): + def __init__(self, config, params): + super().__init__(config, params) + + self.filter_items_cache = {} + self.emby = params["emby"] + self.emby_server_url = self.emby["url"] + self.session = self.config.Requests.session # init? + if self.emby["verify_ssl"] is False and self.config.Requests.global_ssl is True: + logger.debug("Overriding verify_ssl to False for Emby connection") + self.session = self.config.Requests.create_session(verify_ssl=False) + if self.emby["verify_ssl"] is True and self.config.Requests.global_ssl is False: + logger.debug("Overriding verify_ssl to True for Emby connection") + self.session = self.config.Requests.create_session() + self.emby_api_key = self.emby["api_key"] + self.emby_user_id = self.emby["user_id"] + self.overlay_destination_folder = self.emby["overlay_destination_folder"] + self.timeout = self.emby["timeout"] + logger.secret(self.emby_server_url) + logger.secret(self.emby_api_key) + logger.secret(self.emby_user_id) + self.EmbyServer = None + try: + self.EmbyServer = EmbyServer(self.emby_server_url, self.emby_user_id, self.emby_api_key,config, params["name"]) + # timeout not set - self.timeout + logger.info(f"Connected to server {self.EmbyServer.friendlyName} version {self.EmbyServer.version}") + logger.info(f"Running on {self.EmbyServer.platform} version {self.EmbyServer.platformVersion}") + # srv_settings = self.EmbyServer.settings + # try: + # db_cache = srv_settings.get("DatabaseCacheSize") + # logger.info(f"Plex DB cache setting: {db_cache.value} MB") + # if self.plex["db_cache"] and self.plex["db_cache"] != db_cache.value: + # db_cache.set(self.plex["db_cache"]) + # self.PlexServer.settings.save() + # logger.info(f"Plex DB Cache updated to {self.plex['db_cache']} MB") + # except NotFound: + # logger.info(f"Plex DB cache setting: Unknown") + # try: + # chl_num = srv_settings.get("butlerUpdateChannel").value + # if chl_num == "16": + # uc_str = f"Public update channel." + # elif chl_num == "8": + # uc_str = f"PlexPass update channel." + # else: + # uc_str = f"Unknown update channel: {chl_num}." + # except NotFound: + # uc_str = f"Unknown update channel." + # TODO - subscription info + # logger.info(f"PlexPass: {self.EmbyServer.myPlexSubscription} on {uc_str}") + + # try: + # logger.info(f"Scheduled maintenance running between {srv_settings.get('butlerStartHour').value}:00 and {srv_settings.get('butlerEndHour').value}:00") + # except NotFound: + # logger.info("Scheduled maintenance times could not be found") + except Unauthorized: + logger.info(f"Emby Error: Emby connection attempt returned 'Unauthorized'") + raise Failed("Emby Error: Emby API key is invalid") + except ConnectTimeout: + raise Failed(f"Emby Error: Emby did not respond within the {self.timeout}-second timeout.") + except ValueError as e: + logger.info(f"Emby Error: Emby connection attempt returned 'ValueError'") + logger.stacktrace() + raise Failed(f"Emby Error: {e}") + except (ConnectionError, ParseError): + logger.info(f"Emby Error: Emby connection attempt returned 'ConnectionError' or 'ParseError'") + logger.stacktrace() + raise Failed("Emby Error: Plex URL is probably invalid") + + self.Emby = None + + emby_library_names = [] + # print(params) + self.lib_type = None + for s in self.EmbyServer.get_libraries(): + # print(s) + emby_library_names.append(s["Name"]) + if s["CollectionType"] == 'tvshows': + self.lib_type = "show" + elif s["CollectionType"] == 'movies': + self.lib_type = "movie" + if s["Name"] == params["name"]: + self.Emby = s + self.EmbyServer.library_id= self.Emby.get('Id') + print(s) + break + # print(emby_library_names) + if not self.Emby: + raise Failed(f"Emby Error: Emby Library '{params['name']}' not found. Options: {emby_library_names}") + # -------------- + + self.type = self.Emby.get("CollectionType", "") + # Entferne das 's', wenn self.type 'movies' oder 'shows' ist + + # Now, find out the library type + collection_type = self.Emby.get("CollectionType", "").lower() + if collection_type == "movies": + self.emby_type = "Movie" + elif collection_type == "tvshows": + self.emby_type = "Show" + elif collection_type == "music": + self.emby_type = "Artist" + else: + self.emby_type = "Other" + self.type= self.emby_type + # print(f"Collection type is: '{collection_type}'") + # coll = Collection() + if self.emby_type.lower() not in library_types: + raise Failed(f"Emby Error: Emby Library must be a Movies, TV Shows, or Music library") + + + + # print(f"EMBY Library type: {self.type}") + # print(self.type) + self._users = [] + self.emby_users = [] + self._all_items = [] + self._emby_all_items = [] + self._emby_all_items_native = [] + self._account = None + + # source_setting = next((s for s in self.Plex.settings() if s.id in ["ratingsSource"]), None) + # Todo + # print(f"Checkie: {source_setting}") + # Checkie: <Setting:ratingsSource:rottentomatoes> + # Checkie: <Setting:ratingsSource:imdb> + # Checkie: <Setting:ratingsSource:themoviedb> + self.ratings_source = "N/A" # lets' use RT + # self.ratings_source = source_setting.enumValues[source_setting.value] if source_setting else "N/A" + + self.is_movie = self.emby_type == "Movie" + self.is_show = self.emby_type == "Show" + self.is_music = self.emby_type == "Artist" + self.is_other = self.emby_type == "Other" + + # todo: needed for Emby? + if self.is_other and self.type == "Movie": + self.type = "Video" + if not self.is_music and self.update_blank_track_titles: + self.update_blank_track_titles = False + logger.error(f"update_blank_track_titles library operation only works with music libraries") + + logger.info(f"Connected to library {params['name']}") + logger.info(f"Type: {self.type}") + logger.info(f"Ratings Source: {self.ratings_source}") +# ToDo - Untested, develop; use this with db cache instead of set_image_smart + def _upload_image(self, item, image): + upload_success = True + try: + if image.is_url and "theposterdb.com" in image.location: + now = datetime.now() + if self.config.tpdb_timer is not None: + while self.config.tpdb_timer + timedelta(seconds=6) > now: + time.sleep(1) + now = datetime.now() + self.config.tpdb_timer = now + if image.is_poster and image.is_url: + self.upload_poster(item, url=image.location) + elif image.is_poster: + upload_success = self.validate_image_size(image) + if upload_success: + self.upload_poster(item, image.location) + elif image.is_background and image.is_url: + item.uploadArt(url=image.location) + elif image.is_background: + upload_success = self.validate_image_size(image) + if upload_success: + item.uploadArt(filepath=image.location) + elif image.is_url: + item.uploadLogo(url=image.location) + else: + item.uploadLogo(filepath=image.location) + self.reload(item, force=True) + return upload_success + except BadRequest as e: + item.refresh() + raise Failed(e) + + def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True, locked=True, + is_locked=None): + + display = "" + final = "" + attribute_translation[attr] if attr in attribute_translation else attr + "similar" if attr == "similar_artist" else attr + attr_display = attr.replace("_", " ").title() + + if add_tags or remove_tags or sync_tags is not None: + _add_tags = add_tags if add_tags else [] + _remove_tags = remove_tags if remove_tags else [] + _sync_tags = sync_tags if sync_tags else [] + + if attr == "label": + _item_tags = self.EmbyServer.get_emby_item_tags(obj, self.Emby.get("Id"), from_cache=False) + elif attr == "genre": + _item_tags = self.EmbyServer.get_emby_item_genres(obj, self.Emby.get("Id"), from_cache=False) + else: + pass + + _add = [t for t in _add_tags + _sync_tags if t not in _item_tags] + _remove = [t for t in _item_tags if (sync_tags is not None and t not in _sync_tags) or t in _remove_tags] + + # Berechne die finalen Tags + final_tags = sorted(set([t for t in _item_tags if t not in _remove] + _add)) + final_tags = sorted(set(final_tags)) # Entferne eventuelle Duplikate + if final_tags != sorted(set(_item_tags)): + if attr == "label": + self.EmbyServer.set_tags(obj.ratingKey, final_tags) + elif attr == "genre": + self.EmbyServer.set_genres(obj.ratingKey, final_tags) + else: + raise WARNING(f"edit_tags: I won't edit {attr} with {final_tags}") + + if _add: + display += f"+{', +'.join(_add)}" + if _remove: + if display: + display += ", " + display += f"-{', -'.join(_remove)}" + if is_locked is not None and not display and is_locked != locked: + # self.edit_query(obj, {f"{actual}.locked": 1 if locked else 0}) + # todo: add emby locked? + display = "Locked" if locked else "Unlocked" + final = f"{obj.title[:25]:<25} | {attr_display} | {display}" if display else display + if do_print and final: + logger.info(final) + return final[28:] if final else final + + # if add_tags and not remove_tags and not None: + # self.EmbyServer.add_tags(obj.ratingKey, add_tags) + # return + raise WARNING( + f"EMBY EDIT TAGS: {self} - {attr} - {obj} - {add_tags} - {remove_tags} - {sync_tags} - {locked} - {is_locked}") + + display = "" + final = "" + key = attribute_translation[attr] if attr in attribute_translation else attr + actual = "similar" if attr == "similar_artist" else attr + attr_display = attr.replace("_", " ").title() + if add_tags or remove_tags or sync_tags is not None: + _add_tags = add_tags if add_tags else [] + _remove_tags = remove_tags if remove_tags else [] + _sync_tags = sync_tags if sync_tags else [] + try: + obj = self.reload(obj) + _item_tags = [item_tag.tag for item_tag in getattr(obj, key)] + except BadRequest: + _item_tags = [] + _add = [t for t in _add_tags + _sync_tags if t not in _item_tags] + _remove = [t for t in _item_tags if (sync_tags is not None and t not in _sync_tags) or t in _remove_tags] + if _add: + self.tag_edit(obj, actual, _add, locked=locked) + display += f"+{', +'.join(_add)}" + if _remove: + self.tag_edit(obj, actual, _remove, locked=locked, remove=True) + if display: + display += ", " + display += f"-{', -'.join(_remove)}" + if is_locked is not None and not display and is_locked != locked: + self.edit_query(obj, {f"{actual}.locked": 1 if locked else 0}) + display = "Locked" if locked else "Unlocked" + final = f"{obj.title[:25]:<25} | {attr_display} | {display}" if display else display + if do_print and final: + logger.info(final) + return final[28:] if final else final + + def find_poster_url(self, item): + pass + def get_all(self, builder_level=None, load=False): + pass + def get_all_native(self, builder_level=None, load=False): + # todo remove + pass + def get_native_emby_item(self, emby_item_id): + # todo remove + pass + def get_provider_ids(self, item): + # used once, maybe remove + pass + def image_update(self, item, image, tmdb=None, title=None, poster=True): + pass + def item_labels(self, item): + pass + def item_posters(self, item, providers=None): + pass + def notify(self, text, collection=None, critical=True): + pass + def notify_delete(self, message): + pass + def reload(self, item, force=False): + pass + def upload_poster(self, item, image, url=False): + pass + def upload_poster_overlay(self, item, image, url=False): + pass + def upload_background(self, item, image, url=False): + pass \ No newline at end of file diff --git a/modules/emby_server.py b/modules/emby_server.py new file mode 100644 index 000000000..880dde4da --- /dev/null +++ b/modules/emby_server.py @@ -0,0 +1,4560 @@ +import base64 +import hashlib +import os +import re as _re +import time +import urllib.parse +from xml.etree.ElementTree import Element + +import requests +from plexapi.collection import Collection +from plexapi.video import Show, Movie, Episode, Season + +from modules import util +from modules.logs import ERROR +from modules.util import Failed + +logger = util.logger + +# import emby_client +import unicodedata +# from emby_client.rest import ApiException + +# bugs: razzie + berlinale year no poster + +# todo: add person link to collection with type while doing the edits / summary ? + +## Helpful URLS for dev: +# https://swagger.emby.media/?staticview=true#/ +# https://github.com/MediaBrowser/Emby/wiki +# https://dev.emby.media/doc/restapi/Browsing-the-Library.html +# https://docs.mdblist.com/docs/api + +# New class (in developent) for replacing fake Palex objects +class EmbyItem: + def __init__(self, base_item): + pass + + def update_item_info(self, update_date): + pass + + +class PlexMedia: + def __init__(self, name, server_id, item_id, runtime_ticks, provider_ids, media_type, image_tags, + backdrop_image_tags): + self.name = name + self.title = name + self.server_id = server_id + self.ratingKey = item_id + self.runtime_ticks = runtime_ticks + self.provider_ids = provider_ids + self.media_type = media_type + self.image_tags = image_tags + self.backdrop_image_tags = backdrop_image_tags + # self.guid = + # self.smart = True # needed + + def __repr__(self): + return self + + +class Movie(Movie): + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(None, xml_data) + self._loadData(xml_data) + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Video') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + if 'agent' not in data_dict or not data_dict['agent']: + element.set('agent', 'com.plexapp.agents.imdb') # Oder den entsprechenden Agenten + + return element + +class FilterChoiceEmby: + def __init__(self, key, title, thumb=None): + self.key = key + self.title = title + self.thumb = thumb + + +class Show(Show): + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(None, xml_data) + self._loadData(xml_data) + + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Directory') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + if 'agent' not in data_dict or not data_dict['agent']: + element.set('agent', 'com.plexapp.agents.thetvdb') # Oder den entsprechenden Agenten + + return element + + +class Season(Season): + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(None, xml_data) + self._loadData(xml_data) + + def show(self): + pass + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Directory') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + if 'agent' not in data_dict or not data_dict['agent']: + element.set('agent', 'com.plexapp.agents.thetvdb') # Oder den entsprechenden Agenten + + return element + + +class Episode(Episode): + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(None, xml_data) + self._loadData(xml_data) + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Video') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + return element + + +class Collection(Collection): + # music => string error + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(None, xml_data) + self._items = [] # cache for self.items + self._loadData(xml_data) + self.childCount = 0 + + def __len__(self): # pragma: no cover + return len(self.items()) + + def items(self): + """ Returns a list of all items in the collection. """ + return self._items + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Directory') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + return element + + def items(self): + """ Returns a list of all items in the collection. """ + return self._items + + + +class Audio: + def __init__(self, data): + self.data = data + + def __repr__(self): + return self._dict_to_xml(self.data) + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Audio') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + return element + +class Person(Movie): + def __init__(self, data): + xml_data = self._dict_to_xml(data) + super().__init__(xml_data) + self._loadData(xml_data) + + @staticmethod + def _dict_to_xml(data_dict): + element = Element('Person') + for key, value in data_dict.items(): + if value is not None: + element.set(key, str(value)) + if 'agent' not in data_dict or not data_dict['agent']: + element.set('agent', 'com.plexapp.agents.imdb') # Oder den entsprechenden Agenten + + return element + + +class EmbyConfig: + X_EMBY_CONTAINER_SIZE = 50 # Definiere die Standardgröße für die Anzahl der Elemente + + +class EmbyServer: + + #ToDo: use the ProvideId field for saving item rating instead of overwriting custom rating field + def __init__(self, server_url, user_id, api_key, config, library_name = None): + + # ToDo: Merge the cache + self._person_already_demoted = [] + self._items_cache: dict[str, dict] = {} + self._items_cache_fields: dict[str, set[str]] = {} + self._items_cache_ts: dict[str, float] = {} + # anpassbar von außen: self.items_cache_ttl = 0 deaktiviert das Altern (immer frisch) + self.items_cache_ttl: int = 300 + self.cached_person_names = [] + self._person_name_cache = {} + self._bulk_person_cache = {} + self.people_index = {} + self.platformVersion = "" # - undefined?? + self._person_dupe_redirect_last = {} + self._person_dupes_last = {} + self._person_dupes_choice_last = {} + self._http_session = requests.Session() + self.cached_tmdb_ids = {} + self.config = config + self._roman_name_cache = {} # key: tmdb_person_id(str) -> latin_name(str)|None + self.people_cache = {} + self._image_hash_cache = {} + self.people_lib_cache = {} + self.item_cache: dict[int, dict] = {} + self.dirty_items: set[int] = set() # statt Liste self.people_lib_cache = {} + self.emby_genres = None + self.cached_plex_objects = {} + self.all_tags = None + self.cached_runtime = {} + self.cached_locations = {} + self.cached_people = {} + self.cached_studios = {} + self.cached_provider_ids={} + self.file_names = {} + + self.studio_list =None + self.media_by_resolution = {} + self.production_countries = None + self._person_name_fix_cache = set() + + self.production_search = {} + self.emby_server_url = server_url + self.user_id = user_id + self.api_key = api_key + self.headers = {"X-MediaBrowser-Token": api_key} + # To prevent too long URLs, queries are done in batches of n + self.api_batch_size = 50 + self.seconds_between_requests = 0.05 + # get system info to see if it works + self.system_info = self.get_system_info() + self.friendlyName = self.system_info.get("ServerName", "") + self.version = self.system_info.get("Version", "") + self.platform = self.system_info.get("OperatingSystemDisplayName", "") + self.session = requests.Session() + self.year_cache = {} + self.library_id = None + + if library_name: + for s in self.get_libraries(): + if s["Name"] == library_name: + self.library_id = s['Id'] + # print(s) + break + + # configuration = emby_client.Configuration() + # configuration.api_key['api_key'] = self.api_key + # configuration.host = self.emby_server_url + # Uncomment below to setup prefix (e.g. Bearer) for API key, if needed + # configuration.api_key_prefix['api_key'] = 'Bearer' + + # create an instance of the API class + # client = emby_client.ApiClient(configuration) + # activity_log_service = emby_client.ActivityLogServiceApi(client) + # activity_log_service. + # my_test = emby_client.api.user_library_service_api + # pass + # self.cache_filenames() + + # Configure API key authorization: apikeyauth + # configuration = emby_client.Configuration() + # configuration.api_key['api_key'] = api_key + + # Uncomment below to setup prefix (e.g. Bearer) for API key, if needed + # configuration.api_key_prefix['api_key'] = 'Bearer' + + # create an instance of the API class + # client = emby_client.ApiClient(configuration) + + # ------------------------------------------------------------- + # Nutzt DEINE bestehende get_people() – nichts Neues erfunden. + # Fügt nur die fehlenden Bausteine zusammen & integriert library_id. + # ------------------------------------------------------------- + + def _normalize_person(self, p: dict) -> dict: + # Vergleichsrelevante Felder; PrimaryImageTag etc. ignorieren + out = { + "Id": str(p.get("Id")) if p.get("Id") is not None else None, + "Name": p.get("Name"), + "Type": p.get("Type"), + } + if p.get("Type") == "Actor" and p.get("Role"): + out["Role"] = p["Role"] + return out + + def _lists_equal_ordered(self, a: list[dict], b: list[dict]) -> bool: + # Order-sensitiver Vergleich + if len(a) != len(b): + return False + for x, y in zip(a, b): + if self._normalize_person(x) != self._normalize_person(y): + return False + return True + + def _resolve_person_fast(self, people_index, role: str, provider: str, tmdb_id, person_name: str): + """ + Verwendet NUR den vorgewärmten Index (keine HTTP-Calls). + Rückgabe: (emby_id, linked_bool) + - linked_bool True, wenn wir im Person-Objekt eine fehlende ProviderId ergänzen mussten + (nur intern markiert; hier KEIN Update-POST – das machst du weiterhin wo du willst). + """ + prov = (provider or "Tmdb") + tmdb_id_str = str(tmdb_id) + buckets = people_index.get(role) or {} + by_tmdb = buckets.get("by_tmdb", {}) + by_name = buckets.get("by_name", {}) + + # 1) TMDb-Treffer? + if tmdb_id_str in by_tmdb: + person = by_tmdb[tmdb_id_str] + return person.get("Id"), False + + # 2) Name-Treffer? + name_l = (person_name or "").strip().lower() + person = by_name.get(name_l) + if person and person.get("Id"): + # ProviderIds lokal „ergänzen“ (kein Netz-Call hier) + prov_ids = person.get("ProviderIds") or {} + if (prov not in prov_ids) or (str(prov_ids.get(prov)) != tmdb_id_str): + prov_ids[prov] = tmdb_id_str + person["ProviderIds"] = prov_ids + # In-Index auch per tmdb map auffindbar machen + by_tmdb[tmdb_id_str] = person + return person["Id"], True + return person["Id"], False + + # 3) Nichts gefunden + return None, False + + def _pkey(self, p: dict): + """Eindeutiger Schlüssel je Person/Typ für Vergleich.""" + return (str(p.get("Id")) if p.get("Id") is not None else None, p.get("Type")) + + def _pname(self, p: dict): + """Anzeigeformat Name (inkl. Rolle bei Actors).""" + t = p.get("Type") + n = p.get("Name") or "?" + if t == "Actor" and p.get("Role"): + return f"{n} (Actor: {p['Role']})" + return f"{n} ({t})" if t else n + + def summarize_people_changes(self, current_people: list[dict], desired_people: list[dict], + reorder_preview_max: int = 6) -> str: + """Schöner Klartext-Diff für Cast & Crew (inkl. Order-Check).""" + cur_map = {self._pkey(p): p for p in current_people if p.get("Id") and p.get("Type")} + des_map = {self._pkey(p): p for p in desired_people if p.get("Id") and p.get("Type")} + + # Adds & Removes + added_keys = [k for k in des_map.keys() if k not in cur_map] + removed_keys = [k for k in cur_map.keys() if k not in des_map] + + added = [self._pname(des_map[k]) for k in added_keys] + removed = [self._pname(cur_map[k]) for k in removed_keys] + + # Updates (Name/Role) + updates = [] + for k in set(cur_map.keys()).intersection(des_map.keys()): + cur = cur_map[k]; + des = des_map[k] + if (cur.get("Name") or "") != (des.get("Name") or ""): + updates.append(f"Name: {cur.get('Name') or '?'} → {des.get('Name') or '?'} ({des.get('Type')})") + if des.get("Type") == "Actor": + cur_role = cur.get("Role") or "" + des_role = des.get("Role") or "" + if cur_role != des_role: + person = des.get("Name") or cur.get("Name") or "?" + updates.append(f"Role: {person}: {cur_role or '—'} → {des_role or '—'}") + + # Reorder nur wenn keine Adds/Removes + reordered_list = [] + if not added_keys and not removed_keys: + cur_order = [self._pkey(p) for p in current_people if self._pkey(p) in des_map] + des_order = [self._pkey(p) for p in desired_people if self._pkey(p) in cur_map] + if cur_order != des_order: + des_index = {k: i for i, k in enumerate(des_order)} + moved = [k for i, k in enumerate(cur_order) if des_index.get(k) is not None and des_index[k] != i] + names = [self._pname(des_map[k]) for k in moved] + if len(names) > reorder_preview_max: + names = names[:reorder_preview_max] + [f"… +{len(moved) - reorder_preview_max} more"] + reordered_list = names + + parts = [] + if added: + parts.append("+ " + ", ".join(added)) + if removed: + parts.append("- " + ", ".join(removed)) + if updates: + parts.append("~ " + "; ".join(updates)) + if reordered_list: + parts.append("↔ Reordered: " + ", ".join(reordered_list)) + + return " | ".join(parts) if parts else "no changes" + + + def get_years(self, library_id: str): + endpoint = f"/emby/Years?Recursive=True&ParentId={library_id}&api_key={self.api_key}" + url = self.emby_server_url + endpoint + response = requests.get(url, headers=self.headers) + return response.json().get("Items", []) + + def get_official_age_ratings(self, library_id: str): + endpoint = f"/emby/OfficialRatings?Recursive=True&ParentId={library_id}&api_key={self.api_key}" + url = self.emby_server_url + endpoint + response = requests.get(url, headers=self.headers) + return response.json().get("Items", []) + + + def get_resolutions(self): + """ + Fetches years for all items in the database and caches the results. + """ + if not self.file_names: + return [] + # MinWidth + # MinHeight + + my_resolutions = { + "4k": "(?i)2160|4k", + "1080p": "(?i)1080|2k", + "720p": "(?i)720|hd", + "576p": "(?i)576", + "480p": "(?i)480|sd", + # HDR + "hdr": r"(?i)\bHDR10\b", # HDR + + "plus": r'(?i)\bhdr10(\+|p(lus)?\b)', # HDR10+ + "dvhdr": r'(?i)\bdv(.hdr10?\b)', # DV HDR10 + "dvhdrplus": r'(?i)\bdv.HDR10(\+|P(lus)?\b)', # DV HDR10+ + } + + all_choices = [] + found_matches = [] + + # self.media_by_resolution = {} + # Durchsuche die filenames nach Auflösungen und Editionen + for file_key, file_name in self.file_names.items(): + from modules import util + logger = util.logger + # logger.info(f"file name: {file_name}") + # Suche nach Auflösungen + for resolution_key, resolution_regex in my_resolutions.items(): + match = _re.search(resolution_regex, file_name) + if match: + found_regex = match.group(0) + if resolution_key not in self.media_by_resolution: + self.media_by_resolution[resolution_key] = [file_key] + else: + self.media_by_resolution[resolution_key].append(file_key) + if found_regex not in found_matches: + filter_choice = FilterChoiceEmby(key=found_regex, title=resolution_key) + all_choices.append(filter_choice) + found_matches.append(found_regex) + + # Suche nach Editionen + + return all_choices + + def cache_filenames(self, imported_items): + if not imported_items: + return + + for item in imported_items: + # item_media_sources = item.get('MediaSources') + # item_media_path = item.get('Path') + self.file_names[item.get('Id')] = os.path.basename(item.get('Path')) + + self.cached_studios[item.get('Id')] = item.get('Studios') + self.cached_people[item.get('Id')] = item.get('People') + self.cached_locations[item.get('Id')] = item.get('ProductionLocations') # Meet english + self.cached_runtime[item.get('Id')] = item.get('RunTimeTicks') + self.cached_provider_ids[item.get('Id')] = item.get('ProviderIds') + + return + if not self.library_id: + return + if self.file_names is None: + endpoint = f"{self.emby_server_url}/emby/Items" + params = { + "Recursive": "true", + "IncludeItemTypes": "Movie,Series,Episodes", + "ParentId": self.library_id, + "api_key": self.api_key, + "Fields": "Budget,Chapters,DateCreated,Genres,HomePageUrl,IndexOptions,MediaStreams,Overview,ParentId,Path,People,ProviderIds,PrimaryImageAspectRatio,Revenue,SortName,Studios,Taglines" + } + try: + response = requests.get(endpoint, headers=self.headers, params=params) + response.raise_for_status() + items = response.json().get("Items", []) + + for item in items: + # item_media_sources = item.get('MediaSources') + # item_media_path = item.get('Path') + self.file_names[item.get('Id')] = item.get('Path') + self.cached_studios[item.get('Id')] = item.get('Studios') + self.cached_people[item.get('Id')] = item.get('People') + self.cached_locations[item.get('Id')] = item.get('ProductionLocations') # Meet english + self.cached_runtime[item.get('Id')] = item.get('RunTimeTicks') + self.cached_provider_ids[item.get('Id')] = item.get('ProviderIds') + # if _re.search('1080', item_media_path): + # print(the_search) + # print(item_media_path) + + # Collect all IDs for batch year lookup + # self.production_countries += [item["ProductionLocations"] for item in items if "ProductionLocations" in item] + # for item in items: + # self.production_search[item.get('Id')] = item.get('ProductionLocations', []) + # Flache die Liste ab und entferne Duplikate mit Set-Comprehension + # unique_countries = {country for sublist in self.production_countries for country in sublist} + + # Konvertiere das Set zurück in eine (sortierte) Liste + # self.production_countries = sorted(unique_countries) + + # print(f"Resolution cache populated with {len(self.production_countries)} entries.") + except requests.exceptions.RequestException as e: + logger.error(f"Failed to fetch all resolutions: {e}") + # self.production_countries = allcountries + # return self.production_countries + + def get_emby_countries(self, library_id): + """ + Fetches countries for all items in the database and caches the results. + """ + if self.production_countries: + return self.production_countries + self.production_countries=[] + endpoint = f"{self.emby_server_url}/emby/Items" + params = { + "Recursive": "true", + "IncludeItemTypes": "Movie,Series,MusicArtist", + "ParentId": library_id, + "Fields": "ProductionLocations", + "api_key": self.api_key + } + try: + response = requests.get(endpoint, headers=self.headers, params=params) + response.raise_for_status() + items = response.json().get("Items", []) + + # Collect all IDs for batch year lookup + self.production_countries += [item["ProductionLocations"] for item in items if "ProductionLocations" in item] + for item in items: + self.production_search[item.get('Id')] = item.get('ProductionLocations', []) + # Flache die Liste ab und entferne Duplikate mit Set-Comprehension + unique_countries = {country for sublist in self.production_countries for country in sublist} + + # Konvertiere das Set zurück in eine (sortierte) Liste + self.production_countries = sorted(unique_countries) + + logger.info(f"Country cache populated with {len(self.production_countries)} entries.") + except requests.exceptions.RequestException as e: + logger.error(f"Failed to fetch all countries: {e}") + # self.production_countries = allcountries + return self.production_countries + + def get_emby_genres(self, library_id): + """ + Fetches years for all items in the database and caches the results. + """ + if self.emby_genres: + return self.emby_genres + self.emby_genres=[] + endpoint = f"{self.emby_server_url}/emby/Genres" + params = { + "Recursive": "true", + # "IncludeItemTypes": "Movie,Series,MusicArtist", + "ParentId": library_id, + "api_key": self.api_key + } + try: + response = requests.get(endpoint, headers=self.headers, params=params) + response.raise_for_status() + items = response.json().get("Items", []) + + # Collect all IDs for batch year lookup + self.emby_genres += [item["Name"] for item in items if "Name" in item] + + # Konvertiere das Set zurück in eine (sortierte) Liste + # self.emby_genres = sorted(production_countries) + + logger.info(f"Genre cache populated with {len(self.emby_genres)} entries.") + except requests.exceptions.RequestException as e: + logger.error(f"Failed to fetch all genres: {e}") + # self.production_countries = allcountries + return self.emby_genres + + def get_system_info(self): + endpoint = "/emby/System/Info" + url = self.emby_server_url + endpoint + try: + response = requests.get(url, headers=self.headers) + return response.json() + except Exception as e: + logger.error( + f"Error occurred while getting Emby system info, check your configuration. Check your Emby url and port, user ID and API key: {e}" + ) + raise SystemExit + + def get_users(self): + user_list_endpoint = "/emby/Users" + user_list_url = self.emby_server_url + user_list_endpoint + user_list_response = requests.get(user_list_url, headers=self.headers) + try: + return user_list_response.json() + except Exception as e: + logger.error(f"Error occurred while getting users: {e}") + return None + + def update_collection_display_order(self, collection_id, sort_order): + # BUG: There's only one sorting field per item in Emby, while Plex suppoorts individual sortings by collection. So return, do nithing. + # PremiereDate, SortName + # c0e0098d2c574fbee1ee5505dc68f5bd + return + + # sort order not settable in Emby other than item sort name + emby_url_name = f"{self.emby_server_url}/emby/Users/{self.user_id}/Items/{collection_id}?api_key={self.api_key}" + _name = requests.get(emby_url_name, headers=self.headers) + response_name = requests.get(emby_url_name, headers=self.headers) + response_name.raise_for_status() + data_name = response_name.json() + + #added + def get_actor_id(self, name): + """ + Get the actor ID from Emby based on the actor's name. + :param name: The name of the actor to search for. + :return: The ID of the actor if found, else None. + """ + # Define the search endpoint for Emby + search_endpoint = f"/emby/Items" + search_url = self.emby_server_url + search_endpoint + + # Search parameters for Emby + params = { + "SearchTerm": name, + "IncludePeople": "true", + "Recursive": "true" + } + + try: + # Perform the GET request to search for the actor + response = requests.get(search_url, headers=self.headers, params=params) + response.raise_for_status() + + # Parse the response JSON + search_results = response.json() + for result in search_results.get('Items', []): + # Check if the result is a person and matches the name + if result.get("Type") == "Person" and result.get("Name") == name: + # print(f"get_actor_id: {name} - {result.get('Id')}") + # print(result.get("Id")) + return result.get("Id") + + # If no matching actor is found + logger.info(f"Actor not found: {name}") + return None + + except requests.exceptions.RequestException as e: + logger.error(f"Error occurred while searching for actor: {e}") + return None + #added + + def findItems(self, cls=None, initpath=None, parent_id=None,builder_type=None, **kwargs): + """ + Find and build all items from the Emby library that match specified criteria. + + Args: + cls (optional): A class type to filter the items (e.g., Movie, Series, etc.). + initpath (str, optional): The path to initialize the search from. + **kwargs: Additional search attributes to filter items. + + Returns: + list: List of items that match the specified search criteria. + """ + # Initial path to begin search, if specified, otherwise search all items + if parent_id: + items = self.get_items(params={"ParentId": parent_id},include_item_types = [f"{builder_type.capitalize()}"] ) + else: + # print(initpath) + items = self.get_items() + + # If `cls` is specified, filter items by type (e.g., 'Movie', 'Series') + if cls and hasattr(cls, 'TYPE'): + items = [item for item in items if item.get('Type') == cls.TYPE] + + # Apply any additional filters specified in kwargs + filtered_items = [] + for item in items: + match = all(item.get(key, None) == value for key, value in kwargs.items()) + if match: + filtered_items.append(item) + + + return self.convert_emby_to_plex(filtered_items) + # Build list of EmbyCollection instances + + + + def get_items_starting_with_sort_name(self, filter, limit=20): + """ + Retrieves all movies and series whose SortName starts with the specified filter. + Must be queried like this because it's not possible to search for SortName directly. + + Args: + filter (str): The filter to match the SortName against. + + Returns: + list: A list of items (movies and series) whose SortName starts with the filter. + """ + limit = 50 + start_index = 0 + filtered_items = [] + found_sort_name = True + + while found_sort_name: + + items = self.get_items( + fields=["SortName"], + include_item_types=["Movie", "Series"], + sort_by="SortName", + limit=limit, + start_index=start_index, + getAll=False, + ) + + for item in items: + if item["SortName"].startswith(filter): + filtered_items.append(item) + else: + found_sort_name = False + break + + time.sleep(self.seconds_between_requests) + start_index += limit + + return filtered_items + + def get_items_with_imdb_id(self, imdb_ids, item_types=None): + batch_size = self.api_batch_size + returned_items = [] + gotten_item_names = [] + + if item_types is None: + item_types = ["Movie", "Series"] + else: + item_types = [ + ( + "Series" + if item_type.lower() in ["tv", "show"] + else "Movie" if item_type.lower() == "movie" else item_type + ) + for item_type in item_types + ] + + for i in range(0, len(imdb_ids), batch_size): + batch_imdb_ids = imdb_ids[i : i + batch_size] + # Remove any ids from batch_imdb_ids that are None + batch_imdb_ids = [ + imdb_id for imdb_id in batch_imdb_ids if imdb_id is not None + ] + imdb_ids_str = ",".join(["imdb." + imdb_id for imdb_id in batch_imdb_ids]) + + items = self.get_items( + params={"AnyProviderIdEquals": imdb_ids_str}, + fields=["ChildCount", "RecursiveItemCount"], + include_item_types=item_types, + limit=batch_size, + ) + + for item in items: + if item["Name"] not in gotten_item_names: + returned_items.append(item["Id"]) + gotten_item_names.append(item["Name"]) + + return returned_items + + # def get_items_with_tvdb_id(self, tvdb_ids, item_types=None): + # batch_size = self.api_batch_size + # returned_items = [] + # gotten_item_names = [] + # + # if item_types is None: + # item_types = ["Movie", "Series", "Episode"] + # else: + # item_types = [ + # ( + # "Series" + # if item_type.lower() in ["tv", "show"] + # else ( + # "Movie" + # if item_type.lower() == "movie" + # else "Episode" if item_type.lower() == "episode" else item_type + # ) + # ) + # for item_type in item_types + # ] + # + # for i in range(0, len(tvdb_ids), batch_size): + # batch_tvdb_ids = tvdb_ids[i : i + batch_size] + # tvdb_ids_str = ",".join(["tvdb." + tvdb_id for tvdb_id in batch_tvdb_ids]) + # + # items = self.get_items( + # params={"AnyProviderIdEquals": tvdb_ids_str}, + # fields=["ChildCount", "RecursiveItemCount"], + # include_item_types=item_types, + # limit=batch_size, + # ) + # + # for item in items: + # if item["Name"] not in gotten_item_names: + # returned_items.append(item["Id"]) + # gotten_item_names.append(item["Name"]) + # + # return returned_items + + def set_tags(self, item_id, tags: list): + """ + Setzt die Tags eines Items. + """ + return self.__update_item(item_id, { + "Tags": tags, + "TagItems": tags + } + ) + def set_genres(self, item_id, tags: list): + """ + Setzt die Tags eines Items. + """ + return self.__update_item(item_id, { + "Genres": tags, + "GenreItems": tags + } + ) + + + + + def is_in_filtertype(self, tag, libtype): + """ Returns a :class:`~plexapi.library.FilteringType` for a specified libtype. + + Parameters: + libtype (str, optional): The library type to filter (movie, show, season, episode, + artist, album, track, photoalbum, photo, collection). + + Raises: + :exc:`~plexapi.exceptions.NotFound`: Unknown libtype for this library. + """ + + #lib type show: + # ['genre', 'year', 'contentRating', 'studio', 'network', 'country', 'collection', 'director', 'actor', 'writer', 'producer', 'unwatchedLeaves', 'unmatched', 'label'] + + # lib type movie: + # ['genre', 'year', 'decade', 'contentRating', 'collection', 'director', 'actor', 'writer', 'producer', 'country', 'studio', 'resolution', 'hdr', 'unwatched', 'inProgress', 'unmatched', 'audioLanguage', 'subtitleLanguage', 'editionTitle', 'label', 'duplicate'] + + #lib type movie + my_list = [] + if libtype == "show": + my_list = ['genre', 'year', 'contentRating', 'studio', 'network', 'country', 'collection', 'director', 'actor', 'writer', 'producer', 'unwatchedLeaves', 'unmatched', 'label', 'show.label', 'show.studio'] + else: + my_list = ['genre', 'year', 'decade', 'contentRating', 'collection', 'director', 'actor', 'writer', 'producer', 'composer', 'country', 'studio', 'resolution', 'hdr', 'unwatched', 'inProgress', 'unmatched', 'audioLanguage', 'subtitleLanguage', 'editionTitle', 'label', 'duplicate'] + + return tag in my_list + + try: + return next(f for f in self.filterTypes() if f.type == libtype) + except StopIteration: + availableLibtypes = [f.type for f in self.filterTypes()] + from plexapi.exceptions import NotFound + raise NotFound(f'Unknown libtype "{libtype}" for this library. ' + f'Available libtypes: {availableLibtypes}') from None + + + + def get_provider_ids(self, item): + + if str(item.ratingKey) in self.cached_provider_ids: + current_provider_ids = self.cached_provider_ids[str(item.ratingKey)] + else: + emby_item = self.get_item(item.ratingKey) + + # item_type = "" + # Hole die existierenden ProviderIds + current_provider_ids = emby_item.get("ProviderIds", {}) + + normalized_prov_ids = {key.lower(): value for key, value in current_provider_ids.items()} + + imdb = normalized_prov_ids.get("imdb",None) + tvdb = normalized_prov_ids.get("tvdb",None) + tmdb = normalized_prov_ids.get("tmdb",None) + + # item_type = "show" if emby_item.get('Type')=="Series" else "movie" + + return [imdb, tvdb, tmdb] + + + def search(self, title=None, sort=None, maxresults=None, libtype=None, **kwargs): + """ + Searches the Emby server and returns results in batches of 100. + + Args: + title (str): The title to search for. + sort (str): The sort order of the results. + maxresults (int): The maximum number of results to return. + libtype (str): The type of library items to include in the search. + **kwargs: Additional parameters to pass to the search query. + + Returns: + list: A list of search results. + """ + endpoint = "/emby/Items" + batch_size = 100 + start_index = 0 + all_results = [] + + if kwargs == "": + pass + + # Log unknown parameters + valid_params = {"title", "sort", "maxresults", "libtype", "label"} + unknown_params = {k: v for k, v in kwargs.items() if k not in valid_params} + if unknown_params: + raise Warning(f"Unknown parameters passed to EMBY search: {unknown_params}") + my_label = kwargs.get("label", "") + while True: + # Build the query parameters + params = { + "SearchTerm": title, + "SortBy": sort, + "Limit": batch_size, + "StartIndex": start_index, + "IncludeItemTypes": libtype, + "Tags": my_label, + "EnableImages": 'true', + "api_key": self.api_key, + } + # Merge with any additional kwargs + params.update(kwargs) + + # Perform the API request + try: + url = f"{self.emby_server_url}{endpoint}" + response = requests.get(url, headers=self.headers, params=params) + response.raise_for_status() + data = response.json() + batch_results = data.get("Items", []) + all_results.extend(batch_results) + + # If fewer results than the batch size, we are done + if len(batch_results) < batch_size: + break + + # Increment the start index for the next batch + start_index += batch_size + + # Respect the maxresults limit if provided + if maxresults and len(all_results) >= maxresults: + return all_results[:maxresults] + + except requests.exceptions.RequestException as e: + raise ERROR(f"Error occurred during search: {e}") + + return self.convert_emby_to_plex(all_results) + + + def get_boxsets_from_library(self, title=None, library_id= None, label = None, native = False) ->[Collection]: + """ + Retrieve all boxsets from the library with CollectionType 'boxsets'. + Optionally, filter boxsets by a specific title. + + Args: + title (str, optional): The title of the boxset to search for. Defaults to None. + + Returns: + list: A list of Plex-compatible boxset objects. + """ + search_title="" + search_tag="" + get_fields=f"&Fields=ParentId" + if title: + search_title= f"&SearchTerm={urllib.parse.quote(title).replace("&","%26")}" + if label: + search_tag = f"&Tags={urllib.parse.quote(label)}" + # else: + # search_tag = f"&Tags=Kometa" + + # if library_id: + + if not library_id: + library_id = self.library_id + + my_search = f"{self.emby_server_url}/Users/{self.user_id}/Items?Recursive=true{search_title}{search_tag}{get_fields}&ParentId={library_id}&IncludeItemTypes=BoxSet&api_key={self.api_key}" + # my_search = f"{self.emby_server_url}/Items?Recursive=true&SearchTerm={title}&IncludeItemTypes=BoxSets&api_key={self.api_key}" + + title_response = requests.get( + my_search + ).json().get("Items", []) + my_return = list(title_response) + # if len(title_response) > 0: + # for title in title_response: + # if title.get("Type")!= "Boxset": + # my_return.remove(title) + # continue + # my_items= self.get_items_in_boxset(title.get("Id")) + # if len(my_items) == 0: + # self.delete_collection(title.get("Id"),is_id=True) + # my_return.remove(title) + + plex_collections = self.convert_emby_to_plex(my_return, native) + return plex_collections + + # print(f"Title not found in collections: {title}") + # print(title_response) + + # 'https://piratensenderpowerplay.myqnapcloud.com:18096/emby/Items?Recursive=true&SearchTerm=IMDb%20Lowest%20Rated&api_key=1a0798043dc549a5ace4f9a216980308' \ + + + # todo: nur die collections der library finden + # 1. alle collections abfragen + response = requests.get( + f"{self.emby_server_url}/Items?IncludeItemTypes=BoxSet&ParentId={library_id}&api_key={self.api_key}" + ) + collections_with_items = [] + collection_items = response.json().get("Items", []) + + plex_collections = self.convert_emby_to_plex(collection_items) + + logger.info(f"EmbyServer retrieved and converted {len(collection_items)} boxsets from library '{library_id}'.") + + return plex_collections + + + for collection_item in collection_items: + # todo add ParentId as selector? + all_items_in_box_set = self.get_items_in_boxset(collection_item.get("Id",""),native=True) + for item in all_items_in_box_set: + if item.get("ParentId") == library_id: + collections_with_items.append(collection_item) + print(f"{collection_item.get("Name","UNKNOWN")} found in Library") + break + # if collection_item.get("ParentId") == library_id: + + plex_collections = self.convert_emby_to_plex(collections_with_items) + + logger.info(f"Retrieved and converted {len(plex_collections)} boxsets from library '{library_id}'.") + + return plex_collections + + + # 2. alle inhalte der collections abfragen + # 3. alle medien der lib_id abfragen + # 4. abgleich mit den inhalten der collection + # 5. => boxsets from library + + # Schritt 3: Alle Items in der Bibliothek abrufen + response = requests.get( + f"{self.emby_server_url}/Items?ParentId={library_id}&IncludeItemTypes=BoxSet&Recursive=True&s&api_key={self.api_key}" + ) + + if response.status_code != 200: + raise Exception( + f"Failed to retrieve items from library with ID {library_id}. " + f"Response: {response.status_code} - {response.text}" + ) + + items = response.json().get("Items", []) + if not items: + return [] + raise Failed(f"No boxsets found in library '{library_id}'.") + + # print(items) + # Schritt 4: Filtere die Items mit Type 'BoxSet' + # boxsets = [item for item in items if item.get("Type") == "BoxSet"] + + + # Schritt 5: Filtere nach Titel, falls angegeben + + # if not boxsets: + # raise Failed(f"No boxsets matching the title '{title}' were found.") if title else print("No boxsets found.") + + + # Schritt 6: Konvertiere die Boxsets zu Plex-kompatiblen Objekten + plex_collections = self.convert_emby_to_plex(items) + + logger.info(f"Retrieved and converted {len(plex_collections)} boxsets from library '{library_id}'.") + + return plex_collections + + def get_items_in_boxset(self, collection_id: str, include: str = "Movie,Series,Episode,MusicArtist", + fields: str = "ProviderIds,ParentId,ProductionYear,People", native: bool = False): + """ + Retrieves all items in a collection based on the provided collection ID in batches of 100 items. + + Args: + collection_id (str): The ID of the collection. + include (str): The types of items to include. Defaults to "Movie,Series,Episode,MusicArtist". + fields (str): The fields to include in the response. Defaults to "ProviderIds,ParentId,ProductionYear,People". + native (bool): If True, return the native response from Emby. Otherwise, convert to Plex format. + + Returns: + list: A list of items (dictionaries) from the collection, with the year added to each item. + """ + if collection_id is None: + logger.error("Collection ID is None. Cannot fetch items.") + return [] + + if include == "Show": + include = "Series" + + items = [] + batch_size = 100 + start_index = 0 + + while True: + # Build the API endpoint with batching parameters + endpoint = (f"/emby/Items?ParentId={collection_id}&IncludeItemTypes={include}&Fields={fields}&StartIndex={start_index}&Limit={batch_size}&api_key={self.api_key}") + url = self.emby_server_url + endpoint + + try: + # Perform the API request + response = requests.get(url, headers=self.headers) + response.raise_for_status() + items_data = response.json() + batch_items = items_data.get("Items", []) + + # Add the retrieved items to the list + items.extend(batch_items) + + # If fewer than batch_size items were returned, we are at the end + if len(batch_items) < batch_size: + break + + # Increment the start index for the next batch + start_index += batch_size + + except:# requests.exceptions.RequestException as e: + logger.error(f"Error occurred while getting items in collection ID {collection_id}.") + # print(f"Error occurred while getting items in collection ID {collection_id}: {e}") + return [] + break + + if native: + return items + else: + return self.convert_emby_to_plex(items) + + def create_collection(self, collection_name, item_ids, parent_id, locked=False) -> bool: + """ + Check if collection exists, creates if not, then adds items to it in batches. + Must add at least one item when creating collection. + + Args: + collection_name (str): The name of the collection to be created. + item_ids (list): A list of item IDs to be added to the collection. + parent_id (str): The parent ID for the collection. + locked (bool): Whether the collection should be locked after creation. + + Returns: + bool: True if the collection is created successfully, False otherwise. + """ + if not item_ids: + logger.error("Can't create collection, no items to add to it.") + return None + + try: + # item_ids = [str(item_id) for item_id in item_ids] + # Format the collection name for the API + collection_name = collection_name.replace('+', '%2B').replace('&', '%26') + + # Create the collection with the first batch of items + first_batch = item_ids[:100] # Use the first 100 items for initial creation + response = requests.post( + f"{self.emby_server_url}/Collections?api_key={self.api_key}&IsLocked=true&ParentId={parent_id}&Name={collection_name}&Ids={self.__ids_to_str(first_batch)}" + ) + + if response.status_code != 200: + logger.error(f"create_collection: Error creating {collection_name}, response: {response.text}") + return None + + # Parse the response to get the collection ID + data = response.json() + collection_id = data.get('Id') + logger.info(f"Successfully created collection {collection_name}") + + # Process remaining items in batches of 100 + batch_size = 100 + for i in range(batch_size, len(item_ids), batch_size): + batch = item_ids[i:i + batch_size] + # string_ids = ','.join(batch) + batch_response = requests.post( + f"{self.emby_server_url}/Collections/{collection_id}/Items?api_key={self.api_key}&Ids={self.__ids_to_str(batch)}" + ) + + if batch_response.status_code != 204: + logger.error( + f"Error adding batch {i // batch_size + 1} to collection {collection_name}, response: {batch_response.text}") + + # Lock the collection if specified + if locked: + self.lock_collection(collection_id) + + # Optional: Update the display order + self.update_collection_display_order(collection_id, "test") + + time.sleep(1) # Add a short delay to avoid API rate limits + return collection_id + except Exception as e: + logger.error(f"Collection creation failed. - {e}") + + def create_smart_collection(self, title, smart_type, my_items, ignore_blank_results, parent_id): + """ + Emulate smart playlists in Emby by interpreting uri_args, + querying the Emby API for matching items, and adding them to a new collection. + + Args: + title (str): The name of the collection to be created. + smart_type (str): The type of smart playlist (e.g., '1' for movies). + uri_args (str): The URI arguments specifying the filters and sorting. + ignore_blank_results (bool): Whether to proceed if no items match the criteria. + """ + # Parse uri_args + # args = parse_qs(uri_args.lstrip('?')) + + # Initialize Emby API query parameters + emby_query_params = {} + unknown_params = {} + + + + if len(my_items) == 0: + logger.info(f"No items found matching the criteria.") + if not ignore_blank_results: + return None + else: + # Get item IDs + item_ids = [item.ratingKey for item in my_items] + + # Create collection with the found items + return self.create_collection(title, item_ids, parent_id) + print(f"Successfully created smart collection '{title}' with {len(item_ids)} items.") + # Not tested and not working for collections. + def delete_item(self, item_id) -> bool: + """ + Deletes an item from the Emby server. + + Args: + item_id (str): The ID of the item to be deleted. + + Returns: + bool: True if the item is deleted successfully, False otherwise. + """ + + url = f"{self.emby_server_url}/Items?{item_id}&api_key={self.api_key}" + response = requests.delete(url) + if response.status_code == 200: + return True + else: + return False + + # If you need to cache the library property + from functools import cached_property + + @cached_property + def library(self): + """ Library to browse or search your media. """ + try: + data = self.get_library_data(self.library_id) + except Exception as e: + # Handle exceptions as needed + data = {} + return data + + def get_libraries(self): + endpoint = f"{self.emby_server_url}/emby/Library/MediaFolders" + response = self.session.get(endpoint, headers=self.headers) + response.raise_for_status() + # ToDo: maybe add section as Fields + return response.json().get('Items', []) + + def get_library_data(self, library_id): + endpoint = f"{self.emby_server_url}/emby/Library/Sections/{library_id}" + response = self.session.get(endpoint, headers=self.headers) + response.raise_for_status() + return response.json() + + def invalidate_item(self, item_id: int) -> None: + """Von außen aufrufen, wenn du ein Item (z.B. Genres) geändert hast.""" + self.dirty_items.add(item_id) + self.item_cache.pop(item_id, None) + + def _resolve_item_id(self, plex_object) -> int: + # ratingKey auf Plex-Objekten + if hasattr(plex_object, "ratingKey"): + return int(getattr(plex_object, "ratingKey")) + + # direkte ID (int) oder als String + if isinstance(plex_object, int): + return plex_object + if isinstance(plex_object, str): + return int(plex_object.strip()) + + # Korrekt: echten Exception-Typ werfen, NICHT WARNING (logging-level int) + raise Failed(f"Cannot resolve Emby item id from type={type(plex_object).__name__}: {plex_object!r}") + + # ToDo: Merge with fetch_item, also merge item cache + def get_item(self, item_id: int | str, *, force_refresh: bool = False) -> dict | None: + try: + item_id = int(item_id) + except: # will be triggered if item doesnt exist yet + return None + + if not force_refresh and item_id in self.item_cache and item_id not in self.dirty_items: + return self.item_cache[item_id] + + endpoint = f"/emby/users/{self.user_id}/Items/{item_id}?api_key={self.api_key}" + url = self.emby_server_url + endpoint + try: + resp = requests.get(url, headers=self.headers) + resp.raise_for_status() + data = resp.json() + self.item_cache[item_id] = data + self.dirty_items.discard(item_id) # wieder „sauber“ + return data + except Exception as e: + logger.ghost(f"Error occurred while getting item: {e}. URL: {url}.") + return None + + def get_item_images(self, item_id) -> dict: + endpoint = f"/emby/Items/{item_id}/Images?api_key={self.api_key}" + url = self.emby_server_url + endpoint + try: + return requests.get(url, headers=self.headers).json() + except Exception as e: + logger.error(f"Error occurred while getting item image: {e}. URL: {url}.") + return None + + def set_item_property(self, item_id, property_name, property_value): + return self.__update_item(item_id, {property_name: property_value}) + + def get_collection_id(self, collection_name:str): + all_collections = self.get_boxsets_from_library(title=collection_name) + collection_found = False + collection_id = None + for collection in all_collections: + # print(collection) + if collection_name == collection.title: + collection_found = True + collection_id = collection.ratingKey + break + + if not collection_found: + return None + + return collection_id + + def add_to_collection(self, collection_name, item_ids: list) -> int: + # Returns the number of items added to the collection + return self.__add_remove_from_collection(collection_name, item_ids, "add") + + def delete_from_collection(self, collection_name, item_ids: list) -> int: + # Returns the number of items deleted from the collection + return self.__add_remove_from_collection(collection_name, item_ids, "delete") + + def refresh_item(self, item_id): + # Refreshes metadata for a specific item + response = requests.post( + f"{self.emby_server_url}/Items/{item_id}/Refresh?api_key={self.api_key}&ReplaceAllMetadata=true" + ) + time.sleep(self.seconds_between_requests) + if response.status_code != 204: + logger.error(f"Error refreshing item {item_id}, response: {response}") + return False + return True + + def custom_encode(self, params): + """ + Custom encoder to handle the `Tags` parameter without re-encoding certain characters. + """ + encoded_parts = [] + for key, value in params.items(): + # input_string = str(value).replace("+", "%2B") # Apple TV+ + encoded_parts.append(f"{key}={urllib.parse.quote(str(value), safe='|,')}") + return "&".join(encoded_parts) + + def get_items( + self, + params=None, + fields=None, + include_item_types=None, + filters=None, + sort_by=None, + limit=None, + start_index=0, + getAll=True, + ): + """ + Generic method to retrieve all items from Emby, querying in batches. + + Args: + params (dict): Additional parameters to include in the query. + fields (list): List of fields to include in the response. + include_item_types (list): Types of items to include (e.g., ['Movie', 'Series']). + filters (list): Filters to apply to the query. + sort_by (str): Field to sort the results by. + limit (int): Number of items to query in each batch. + start_index (int): Index to start querying from. + getAll (bool): Flag to indicate whether to retrieve all items or just the first batch. + + Returns: + list: All items retrieved from the Emby API. + """ + endpoint = f"/emby/Users/{self.user_id}/Items" + query_params = {} + query_params["api_key"] = self.api_key + + if fields: + query_params["Fields"] = fields + else: + query_params["Fields"] = "Studios,CustomRating,CriticRating,CommunityRating" + + if include_item_types: + query_params["IncludeItemTypes"] = ",".join(include_item_types) + if filters: + query_params["Filters"] = ",".join(filters) + if sort_by: + query_params["SortBy"] = sort_by + if limit: + query_params["Limit"] = limit + + if params: + query_params.update(params) + + if "Tags" in query_params: + query_params["Tags"] = query_params["Tags"].replace("&", "%26") + + + if "Studios" in query_params: + the_studio = self.get_library_studios(query_params["Studios"]) + if len(the_studio) == 0: + return [] + + limit_query = 100 + if not "Ids" in query_params: + query_params["Recursive"] = "true" + + url = self.emby_server_url + endpoint + all_items = [] + + if "Ids" in query_params: + ids = query_params["Ids"].split(",") + batches = [ids[i:i + limit_query] for i in range(0, len(ids), limit_query)] + else: + batches = [None] # Single batch when no Ids are specified + + for batch in batches: + if batch: + query_params["Ids"] = ",".join(batch) + start_index = 0 + + while True: + time.sleep(self.seconds_between_requests) + query_params["StartIndex"] = start_index + encoded_query = self.custom_encode(query_params) + full_url = f"{url}?{encoded_query}" + + response = requests.get(full_url, headers=self.headers) + try: + response_data = response.json() + except Exception as e: + logger.error( + f"Error getting items using URL {url} params {query_params} with response {response.content}. Error: {e}" + ) + return None + + if "Items" in response_data: + items = response_data["Items"] + all_items.extend(items) + if len(items) < limit_query: + break # All items retrieved + start_index += limit_query + else: + break + + if not getAll: + break + + # Filter and keep items based on ratings or studios + filtered_results = list(all_items) + if "SortBy" in query_params: + sort_key = query_params["SortBy"] + reverse_order = query_params.get("SortOrder", "Ascending").lower() == "descending" + + try: + # Sortiere basierend auf dem SortBy-Schlüssel + filtered_results.sort(key=lambda item: item.get(sort_key, 0), reverse=reverse_order) + except Exception as e: + logger.error(f"Error during sorting by {sort_key}: {e}") + raise Failed(f"Sorting failed for key: {sort_key}") + if ( + "MaxCriticRating" in query_params or "MaxCommunityRating" in query_params or "MaxCustomRating" in query_params or + "MinCriticRating" in query_params or "MinCommunityRating" in query_params or "MinCustomRating" in query_params + ): + updated_results = [] + + for item in all_items: + keep_item = True + if "MaxCriticRating" in query_params: + critic_rating = int(item.get("CriticRating", 0)) + max_rating = int(query_params.get("MaxCriticRating")) + if critic_rating > max_rating or critic_rating == 0: + keep_item = False + + if "MaxCommunityRating" in query_params: + community_rating = float(item.get("CommunityRating", 0)) + max_rating = float(query_params.get("MaxCommunityRating")) + if community_rating > max_rating or community_rating == 0: + keep_item = False + + if "MaxCustomRating" in query_params: + custom_rating = float(item.get("CustomRating", 0)) + max_rating = float(query_params.get("MaxCustomRating")) + if custom_rating > max_rating or custom_rating == 0: + keep_item = False + + if "MinCriticRating" in query_params: + critic_rating = int(item.get("CriticRating", 0)) + min_rating = int(query_params.get("MinCriticRating")) + if critic_rating < min_rating or critic_rating == 0: + keep_item = False + + if "MinCommunityRating" in query_params: + community_rating = float(item.get("CommunityRating", 0)) + min_rating = float(query_params.get("MinCommunityRating")) + if community_rating < min_rating or community_rating == 0: + keep_item = False + + if "MinCustomRating" in query_params: + custom_rating = float(item.get("CustomRating", 0)) + min_rating = float(query_params.get("MinCustomRating")) + if custom_rating < min_rating or custom_rating == 0: + keep_item = False + + if keep_item: + updated_results.append(item) + + filtered_results = updated_results + + # Studio search logic + if "Studios" in query_params: + studio_filter_results = [] + studio_names = query_params["Studios"].split(",") + for item in filtered_results: + item_studios = item.get("Studios", []) + for studio in item_studios: + if studio.get("Name") in studio_names: + studio_filter_results.append(item) + break # Avoid duplicate entries for the same item + filtered_results = studio_filter_results + + if "Limit" in query_params: + try: + # Konvertiere Limit in eine Zahl und begrenze die Anzahl der zurückgegebenen Ergebnisse + limit = int(query_params["Limit"]) + filtered_results = filtered_results[:limit] + except ValueError: + logger.error(f"Invalid Limit value: {query_params['Limit']}") + raise Failed(f"Invalid Limit value: {query_params['Limit']}") + return filtered_results + + def set_item_as_played(self, user_id, item_id): + """ + Set an item as played for a specific user. + + Args: + user_id (str): The ID of the user. + item_id (str): The ID of the item to mark as played. + + Returns: + bool: True if the item was marked as played successfully, False otherwise. + """ + endpoint = f"/emby/Users/{user_id}/PlayedItems/{item_id}" + url = self.emby_server_url + endpoint + response = requests.post(url, headers=self.headers) + if response.status_code == 200: + return True + else: + logger.error( + f"Error marking item {item_id} as played for user {user_id}: {response.content}" + ) + return False + + def set_item_as_favorite(self, user_id, item_id): + """ + Set an item as a favorite for a specific user. + + Args: + user_id (str): The ID of the user. + item_id (str): The ID of the item to mark as a favorite. + + Returns: + bool: True if the item was marked as a favorite successfully, False otherwise. + """ + endpoint = f"/emby/Users/{user_id}/FavoriteItems/{item_id}" + url = self.emby_server_url + endpoint + response = requests.post(url, headers=self.headers) + if response.status_code == 200: + return True + else: + logger.error( + f"Error marking item {item_id} as a favorite for user {user_id}: {response.content}" + ) + return False + + def set_image_smart( + self, + item_id: str, + image_path: str, + image_type: str = "Primary" + ) -> bool: + if image_path.startswith("http"): + return self.__set_remote_image(item_id, image_path, image_type) + else: + return self.__upload_image(item_id, image_path, image_type) + + + def __set_remote_image( + self, + item_id, + image_url, + image_type="Primary", + ): + """ + Downloads a remote image for an item. + + Args: + item_id (str): The ID of the item. + image_url (str): The URL of the image to download. + image_type (str): The type of the image. Defaults to "Primary". + provider_name (str): The name of the image provider. Defaults to "MDBList Collection Creator script". + + Returns: + bool: True if the image is downloaded successfully, False otherwise. + """ + endpoint = f"/emby/Items/{item_id}/RemoteImages/Download" + url = self.emby_server_url + endpoint + + params = { + "Type": image_type, + "ImageUrl": image_url, + } + + try: + response = requests.post( + url, + headers=self.headers, + json=params, + ) + + if response.status_code == 204: + return True + else: + # logger.error(f"Error setting image for item {item_id}, response: {response}") + return False + + except Exception as e: + logger.error(f"Exception occurred while downloading image: {str(e)}") + return False + + def __upload_image(self, item_id, image_path, image_type="Primary"): + """ + Uploads a poster image to a collection. Allows for .jpg, .jpeg, .png, and .tbn formats. + + Args: + item_id (str): The ID of the item. + image_path (str): The path to the image to upload. + image_type (str): The type of the image. Defaults to "Primary". + + Returns: + bool: True if the image is uploaded successfully, False otherwise. + """ + + if not os.path.exists(image_path): + logger.error(f"Error: Image file not found: {image_path}") + return False + + + ext_to_content_type = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".tbn": "image/jpeg", # .tbn ist technisch JPEG + ".webp": "image/webp", + } + + ext = os.path.splitext(image_path)[1].lower() + if ext not in ext_to_content_type: + logger.error(f"Unsupported image format. Must be one of: {', '.join(ext_to_content_type.keys())}") + return False + + try: + with open(image_path, "rb") as f: + image_data = f.read() + + image_data_base64 = base64.b64encode(image_data) + + endpoint = ( + f"/emby/Items/{item_id}/Images/{image_type}?api_key={self.api_key}" + ) + url = self.emby_server_url + endpoint + headers = { + "Content-Type": "image/jpeg", + "X-Emby-Token": self.api_key, + } + + response = requests.post( + url, + headers=headers, + data=image_data_base64, + ) + + if response.status_code == 204: + return True + else: + logger.error(f"Error uploading image for item {item_id}, response: {response}") + return False + + except Exception as e: + logger.error(f"Exception occurred while uploading image: {str(e)}") + return False + + # Konvertierungsfunktion, um Emby-Daten in Plex-Klassenobjekte zu konvertieren + def convert_emby_to_plex(self, emby_data_list, use_native_emby = False): + plex_objects = [] + if not emby_data_list or len(emby_data_list) == 0: + return [] + + # emby_data_list = sorted(set(emby_data_list)) + for item in emby_data_list: + # print(item) + if item is None: + logger.info("Item is None") + continue + if item.get("Id") in self.cached_plex_objects: + plex_object = self.cached_plex_objects[item.get("Id")] + else: + + # if isinstance(item, str): + # print("Ich bin string!!!") + name = item.get("Name") + studio_entries =item.get("Studios") + studio = "" + if studio_entries: + # fix multiple studios when updating, only one allowed + if len(studio_entries) > 1: + for entry in studio_entries: + studio += f"{entry.get('Name')}" + else: + studio= studio_entries[0].get("Name") + + genres = item.get("Genres") + server_id = item.get("ServerId") + item_id = item.get("Id") + item_overview = item.get("Overview") + runtime_ticks = item.get("RunTimeTicks") + if runtime_ticks: + runtime_ticks= int(runtime_ticks) / 10000 + # provider_ids = item.get("ProviderIds", {}) + media_type = item.get("Type") + image_tags = item.get("ImageTags", {}) + backdrop_image_tags = item.get("BackdropImageTags", []) + year = item.get("ProductionYear", None) + series_id = item.get("SeriesId") + if year: + year= int(year) + people = item.get("People", []) + plex_actors = [] + # for person in people: + # # List[PlexObjectT] + # if person.get('Type') == 'Actor': + # new_person = PLEXOBJECTS.get("actor")() + # # new_person.tag = 'actor' + # new_person.id = person.get('Id') + # new_person.name = person.get('Name') + # plex_actors.append(new_person) + # {'Id': '12917', 'Name': 'Hideko Takamine', 'PrimaryImageTag': '243089fe465d5f5402eb21462e0d915e', 'Role': 'Keiko Yashiro', 'Type': 'Actor'} + # Erstelle ein Datenobjekt, das der Plex-API-Struktur ähnelt + crit_rat = item.get("CriticRating") + if crit_rat: # prevent constant updates, fake plex its original value + crit_rat = float(crit_rat) /10 + data = { + 'title': name, + 'ratingKey': item_id, + 'duration': runtime_ticks, + 'guid': item_id, + 'posterUrl': image_tags.get('Primary', ''), + 'thumbUrl': image_tags.get('Art', ''), + 'type': media_type.lower(), + 'backdrop': ','.join(backdrop_image_tags), + 'year': year, + 'summary':item_overview, + 'grandparentTitle':item.get('SeriesName'), + 'audienceRating': item.get("CommunityRating"), + 'rating': crit_rat, + 'userRating': item.get("CustomRating"), + 'studio': studio, # only one + 'genres': genres, + 'titleSort': item.get('SortName'), + # not working + # 'media': [item.get('Path')], + # 'locations':[item.get('Path')] # todo: working? + # 'roles':plex_actors + } + if item.get("Type") == "Series": + + pass + elif item.get("Type") == "Season": + new_data={ + 'index': item.get('IndexNumber'), + 'parentTitle': item.get('SeriesName') + } + data.update(new_data) + pass + elif item.get("Type") == "Episode": + pass + + # print(media_type) + if media_type == "Movie": + plex_object = Movie(data) + elif media_type == "Series": + plex_object = Show(data) + elif media_type == "Season": + plex_object= Season(data) + elif media_type == "Episode": + plex_object=Episode(data) + elif media_type == "BoxSet": + new_col = Collection(data) + if not use_native_emby: + allitems = self.get_items_in_boxset(new_col.ratingKey) + new_col._items = allitems + new_col.childCount= len((allitems)) + + plex_object=new_col + + elif media_type == "Audio": + plex_object=Audio(data) + elif media_type == "Person": + plex_object=Person(data) + else: + logger.error(f"error converting Emby object") + continue + plex_object.locations = [item.get("Path", [])] + if not use_native_emby: + self.cached_plex_objects[item.get("Id")] = plex_object + + plex_objects.append(plex_object) + # else: + # plex_objects.append( + # PlexMedia(name, server_id, item_id, runtime_ticks, provider_ids, media_type, image_tags, + # backdrop_image_tags)) + + return plex_objects + + + def add_tags(self, item_id:int, add_tags:[]): + """ + Fügt einen Tag hinzu. + """ + item = self.get_item(item_id) + if not item: + return False + current_tags = item.get("TagItems", []) + new_tags = [] + for tag_item in current_tags: + new_tags.append(tag_item.get('Name')) + + for tag in add_tags: + if tag not in new_tags: + new_tags.append(tag) + + # db_tags = ",".join(new_tags) + + + return self.__update_item(item_id, {"Tags": new_tags, "TagItems": new_tags}, item) + + + def remove_tags(self, item_id, tags): + """ + Entfernt einen Tag. + """ + item = self.get_item(item_id) + if not item: + return False + + current_tags = item.get("TagItems", []) + new_tags = [] + for tag_item in current_tags: + if tag_item.get('Name') not in tags: + new_tags.append(tag_item.get('Name')) + + + return self.__update_item(item_id, {"Tags": new_tags, "TagItems": new_tags}, item) + + def update_item(self, item_id, data): + return self.__update_item(item_id,data) + + def __update_item(self, item_id, data, item = None): + if not item: + item = self.get_item(item_id) + if item is None: + return None + self._ensure_http_session() + + if "LockedFields" not in item: + item["LockedFields"] = [] + + if "ForcedSortName" in data: + item["ForcedSortName"] = data["ForcedSortName"] + item["SortName"] = data["ForcedSortName"] + + if "SortName" not in item["LockedFields"]: + item["LockedFields"].append("SortName") + + unchanged = all(item.get(k) == v for k, v in data.items()) + if unchanged: + return None + + item.update(data) + + update_item_url = ( + f"{self.emby_server_url}/emby/Items/{item_id}?api_key={self.api_key}" + ) + my_= str(item) + try: + response = requests.post(update_item_url, json=item) + # print( + # f"Updated item {item_id} with {data}. Waiting {self.seconds_between_requests} seconds." + # ) + # time.sleep(self.seconds_between_requests) + self.invalidate_item(item_id) + return response + except Exception as e: + logger.warning(f"Emby item not longer present, not updated: {item_id}") + return None + + def __add_remove_from_collection( + self, collection_name: str, item_ids: list, operation: str + ) -> int: + + affected_count = 0 + if not item_ids: + return None + + collection_id = self.get_collection_id(collection_name) + + if collection_id is None: + return None + + batch_size = self.api_batch_size + num_batches = (len(item_ids) + batch_size - 1) // batch_size + + logger.info( + f"Processing {collection_name} with '{operation}' in {num_batches} batches" + ) + + for i in range(num_batches): + start_index = i * batch_size + end_index = min((i + 1) * batch_size, len(item_ids)) + batch_item_ids = item_ids[start_index:end_index] + # print(".", end="", flush=True) + + if operation == "add": + response = requests.post( + f"{self.emby_server_url}/Collections/{collection_id}/Items/?api_key={self.api_key}&Ids={self.__ids_to_str(batch_item_ids)}" + ) + elif operation == "delete": + response = requests.delete( + f"{self.emby_server_url}/Collections/{collection_id}/Items/?api_key={self.api_key}&Ids={self.__ids_to_str(batch_item_ids)}" + ) + + if response.status_code != 204: + logger.error( + f"Error processing collection with operation '{operation}', response: {response}" + ) + return affected_count + + affected_count += len(batch_item_ids) + time.sleep(self.seconds_between_requests) + + # print() + logger.info(f"Finished '{operation}' with {len(item_ids)} items in {collection_name}") + + return affected_count + + def add_remove_plex_object_from_collection( + self, collection_name: str, plex_objects: list, operation: str, collection_id = None) -> int: + item_ids = [] + for item in plex_objects: + item_ids.append(item.ratingKey) + + affected_count = 0 + if not item_ids: + return affected_count + + if not collection_id: + collection_id = self.get_collection_id(collection_name) + + if collection_id is None: + return affected_count + + batch_size = self.api_batch_size + num_batches = (len(item_ids) + batch_size - 1) // batch_size + + logger.info( + f"Processing {collection_name} with '{operation}' in {num_batches} batches" + ) + + for i in range(num_batches): + start_index = i * batch_size + end_index = min((i + 1) * batch_size, len(item_ids)) + batch_item_ids = item_ids[start_index:end_index] + # print(".", end="", flush=True) + + if operation == "add": + response = requests.post( + f"{self.emby_server_url}/Collections/{collection_id}/Items/?api_key={self.api_key}&Ids={self.__ids_to_str(batch_item_ids)}" + ) + elif operation == "delete": + response = requests.delete( + f"{self.emby_server_url}/Collections/{collection_id}/Items/?api_key={self.api_key}&Ids={self.__ids_to_str(batch_item_ids)}" + ) + + if response.status_code != 204: + logger.error( + f"Error processing collection with operation '{operation}', response: {response}" + ) + return affected_count + + affected_count += len(batch_item_ids) + time.sleep(self.seconds_between_requests) + + # print() + logger.info(f"Finished '{operation}' with {len(item_ids)} items in {collection_name}") + + return affected_count + + def delete_collection(self, collection,is_id = False): + if is_id: + collection_id = collection + else: + collection_id = collection.ratingKey + delete_url = f'{self.emby_server_url}/Items/{collection_id}/Delete?api_key={self.api_key}' + headers = { 'accept': '*/*'} + response = requests.post(delete_url, headers=headers) + if response.status_code == 204: + logger.info(f'Successfully deleted collection with ID "{collection_id}"') + else: + + # response = requests.get(f"{self.emby_server_url}/Items?Recursive=true&ParentId={collection_id}", headers=self.headers) + # response.raise_for_status() + # all_items = response.json().get("Items", []) + all_items = self.get_items_in_boxset(collection_id,native=True) + batch_size = 100 # Größe der Batches + all_ids = [item.get("Id") for item in all_items] + # Teile `all_ids` in Batches auf + for i in range(0, len(all_ids), batch_size): + batch_ids = all_ids[i:i + batch_size] # Nimm einen Batch von 100 IDs + string_ids = ','.join(batch_ids) # Verbinde die IDs zu einem String + response = requests.delete( + f"{self.emby_server_url}/emby/Collections/{collection_id}/Items?Ids={string_ids}&api_key={self.api_key}" + ) + + # Optional: Überprüfe die Antwort und logge den Status + if response.status_code == 204: + logger.ghost(f"Batch {i // batch_size + 1}: Erfolgreich gelöscht") + else: + logger.ghost(f"Batch {i // batch_size + 1}: Fehler beim Löschen - {response.status_code} - {response.text}") + # all_ids = [item.get("Id") for item in all_items] + # print(f'Error deleting collection with ID "{collection_id}": {response.text}') + # def remove_tags_from_collection( + # self, collection_name: str, tags_to_remove: list + # ) -> int: + # """ + # Removes specific tags from all items in a given Emby collection. + # + # Args: + # collection_name (str): The name of the collection. + # tags_to_remove (list): A list of tags to remove from items in the collection. + # + # Returns: + # int: The number of items updated. + # """ + # updated_count = 0 + # + # # Retrieve the collection ID + # collection_id = self.get_collection_id(collection_name) + # + # if collection_id is None: + # print(f"Collection '{collection_name}' not found.") + # return updated_count + # + # # Get all items in the collection + # response = requests.get( + # f"{self.emby_server_url}/Collections/{collection_id}/Items?api_key={self.api_key}" + # ) + # + # if response.status_code != 200: + # print( + # f"Failed to retrieve items from collection '{collection_name}'. Response: {response.status_code} - {response.text}" + # ) + # return updated_count + # + # items = response.json().get("Items", []) + # + # if not items: + # print(f"No items found in collection '{collection_name}'.") + # return updated_count + # + # print(f"Removing tags {tags_to_remove} from {len(items)} items in '{collection_name}'.") + # + # # Iterate over items and remove tags + # for item in items: + # item_id = item["Id"] + # item_response = requests.get( + # f"{self.emby_server_url}/Items/{item_id}?api_key={self.api_key}" + # ) + # + # if item_response.status_code != 200: + # print( + # f"Failed to retrieve details for item ID {item_id}. Response: {item_response.status_code} - {item_response.text}" + # ) + # continue + # + # item_details = item_response.json() + # current_tags = item_details.get("Tags", []) + # + # # Remove the specified tags + # updated_tags = [tag for tag in current_tags if tag not in tags_to_remove] + # + # if set(current_tags) == set(updated_tags): + # # Skip if no tags need to be removed + # continue + # + # # Update the item's tags + # update_response = requests.post( + # f"{self.emby_server_url}/Items/{item_id}?api_key={self.api_key}", + # json={"Tags": updated_tags}, + # ) + # + # if update_response.status_code == 204: + # updated_count += 1 + # print(f"Tags updated for item ID {item_id}.") + # else: + # print( + # f"Failed to update tags for item ID {item_id}. Response: {update_response.status_code} - {update_response.text}" + # ) + # + # time.sleep(self.seconds_between_requests) + # + # print(f"Finished removing tags from {updated_count} items in '{collection_name}'.") + # return updated_count + + # def remove_collection(self, collection_name: str, locked: bool = True) -> None: + # """ + # Remove a collection in Emby. + # + # Args: + # collection_name (str): Name of the collection to remove. + # locked (bool): True to lock items after removal, False otherwise. + # + # Returns: + # None + # """ + # # Retrieve the collection ID + # collection_id = self.get_collection_id(collection_name) + # + # if not collection_id: + # print(f"Collection '{collection_name}' does not exist.") + # return + # + # # Remove the collection + # response = requests.delete( + # f"{self.emby_server_url}/Collections/{collection_id}?api_key={self.api_key}" + # ) + # + # if response.status_code != 204: + # raise Exception( + # f"Failed to delete collection '{collection_name}'. " + # f"Response: {response.status_code} - {response.text}" + # ) + # + # print(f"Collection '{collection_name}' successfully removed.") + + # def remove_kometa_tags_from_collection(self, collection_name: str, locked=True) -> int: + # """ + # Remove tags starting with 'kometa_' from all items in a given Emby collection. + # + # Args: + # collection_name (str): Name of the collection. + # locked (bool): True to lock the tags field after removal, False otherwise. + # + # Returns: + # int: The number of items that had tags removed. + # """ + # updated_count = 0 + # + # # Retrieve the collection ID + # collection_id = self.get_collection_id(collection_name) + # + # if collection_id is None: + # print(f"Collection '{collection_name}' not found.") + # return updated_count + # + # # Get all items in the collection + # response = requests.get( + # f"{self.emby_server_url}/Collections/{collection_id}/Items?api_key={self.api_key}" + # ) + # + # if response.status_code != 200: + # print( + # f"Failed to retrieve items from collection '{collection_name}'. Response: {response.status_code} - {response.text}" + # ) + # return updated_count + # + # items = response.json().get("Items", []) + # + # if not items: + # print(f"No items found in collection '{collection_name}'.") + # return updated_count + # + # print(f"Removing 'kometa_' tags from {len(items)} items in '{collection_name}'.") + # + # # Iterate over items and remove 'kometa_' tags + # for item in items: + # item_id = item["Id"] + # item_response = requests.get( + # f"{self.emby_server_url}/Items/{item_id}?api_key={self.api_key}" + # ) + # + # if item_response.status_code != 200: + # print( + # f"Failed to retrieve details for item ID {item_id}. Response: {item_response.status_code} - {item_response.text}" + # ) + # continue + # + # item_details = item_response.json() + # current_tags = item_details.get("Tags", []) + # + # # Filter out tags that start with 'kometa_' + # updated_tags = [tag for tag in current_tags if not tag.startswith("kometa_")] + # + # if set(current_tags) == set(updated_tags): + # # Skip if no 'kometa_' tags need to be removed + # continue + # + # # Update the item's tags + # payload = {"Tags": updated_tags} + # if locked: + # payload["LockTags"] = True + # + # update_response = requests.post( + # f"{self.emby_server_url}/Items/{item_id}?api_key={self.api_key}", + # json=payload, + # ) + # + # if update_response.status_code == 204: + # updated_count += 1 + # print(f"'kometa_' tags updated for item ID {item_id}.") + # else: + # print( + # f"Failed to update tags for item ID {item_id}. Response: {update_response.status_code} - {update_response.text}" + # ) + # + # time.sleep(self.seconds_between_requests) + # + # print(f"Finished removing 'kometa_' tags from {updated_count} items in '{collection_name}'.") + # return updated_count + + @staticmethod + def __ids_to_str(ids: list) -> str: + item_ids = [str(item_id) for item_id in ids] + return ",".join(item_ids) + + # def add_collection(self, collection_name: str, item_ids: list, locked: bool = True) -> None: + # """ + # Add a collection in Emby. + # + # Args: + # collection_name (str): The name of the collection to create or update. + # item_ids (list): List of item IDs to add to the collection. + # locked (bool): True to lock the collection, False otherwise. + # + # Returns: + # None + # """ + # # Check if collection exists + # collection_id = self.get_collection_id(collection_name) + # + # if not collection_id: + # # Create a new collection if it doesn't exist + # response = requests.post( + # f"{self.emby_server_url}/Collections?api_key={self.api_key}", + # json={"Name": collection_name} + # ) + # + # if response.status_code != 200: + # raise Exception( + # f"Failed to create collection '{collection_name}'. " + # f"Response: {response.status_code} - {response.text}" + # ) + # + # collection_id = response.json().get("Id") + # + # # Add items to the collection + # response = requests.post( + # f"{self.emby_server_url}/Collections/{collection_id}/Items?api_key={self.api_key}&Ids={','.join(map(str, item_ids))}" + # ) + # + # if response.status_code != 204: + # raise Exception( + # f"Failed to add items to collection '{collection_name}'. " + # f"Response: {response.status_code} - {response.text}" + # ) + # + # # Lock the collection (if applicable) + # if locked: + # self.lock_collection(collection_id) + + + + def lock_collection(self, collection_id: str) -> None: + """ + Lock a collection in Emby. + + Args: + collection_id (str): ID of the collection to lock. + + Returns: + None + """ + response = requests.post( + f"{self.emby_server_url}/Items/{collection_id}?api_key={self.api_key}", + json={"LockData": True} + ) + + if response.status_code != 204: + raise Exception( + f"Failed to lock collection ID {collection_id}. " + f"Response: {response.status_code} - {response.text}" + ) + + + def remove_boxset(self, collection_name, collection_id = None): + """ + Remove a BoxSet collection from Emby. + + Args: + collection_id (str): The ID of the collection (BoxSet) to remove. + + Returns: + bool: True if the BoxSet was successfully removed, False otherwise. + """ + if collection_id is None: + collection_id = self.get_collection_id(collection_name) + try: + # API-Endpunkt für das Löschen einer Sammlung + response = requests.delete( + f"{self.emby_server_url}/Items/{collection_id}?api_key={self.api_key}" + ) + + if response.status_code == 204: + logger.info(f"BoxSet with ID '{collection_id}' successfully removed.") + return True + else: + logger.error( + f"Failed to remove BoxSet with ID '{collection_id}'. Response: {response.status_code} - {response.text}") + return False + + except Exception as e: + logger.error(f"Error removing BoxSet with ID '{collection_id}': {e}") + return False + + def get_emby_item_tags(self, plex_object, library_id: str = "", search_all: bool = False, + from_cache: bool = True) -> list[str]: + # Item-spezifisch (häufigster Fall) + if not search_all: + item_id = self._resolve_item_id(plex_object) + item = self.get_item(item_id, force_refresh=(item_id in self.dirty_items)) + if not item: + return [] + # Emby liefert entweder "Tags": ["A","B"] oder "TagItems": [{"Name":"A"}, ...] + if isinstance(item.get("Tags"), list): + tags = {t for t in item["Tags"] if t} + else: + tags = {ti.get("Name") for ti in (item.get("TagItems") or []) if ti.get("Name")} + return sorted(tags) + + # Library-weit: erst versuchen, aus bereits gecachten Items zu aggregieren + agg = set() + for cached in self.item_cache.values(): + if library_id and str(cached.get("LibraryId") or cached.get("ParentId") or "") != str(library_id): + continue + if isinstance(cached.get("Tags"), list): + agg.update({t for t in cached["Tags"] if t}) + else: + agg.update({ti.get("Name") for ti in (cached.get("TagItems") or []) if ti.get("Name")}) + if agg: + return sorted(agg) + + # Fallback: einmaliger API-Call (falls nichts im Cache bekannt ist) + url = f"{self.emby_server_url}/emby/Tags?Recursive=true&ParentId={library_id}&api_key={self.api_key}" + data = requests.get(url, headers=self.headers).json() + return sorted({it.get("Name") for it in data.get("Items", []) if it.get("Name")}) + + def get_emby_item_genres(self, plex_object, library_id: str = "", search_all: bool = False, + from_cache: bool = True) -> list[str]: + if not search_all: + item_id = self._resolve_item_id(plex_object) + item = self.get_item(item_id, force_refresh=(item_id in self.dirty_items)) + if not item: + return [] + # "Genres": ["Comedy"] oder "GenreItems": [{"Name":"Comedy"}] + if isinstance(item.get("Genres"), list): + genres = {g for g in item["Genres"] if g} + else: + genres = {gi.get("Name") for gi in (item.get("GenreItems") or []) if gi.get("Name")} + return sorted(genres) + + agg = set() + for cached in self.item_cache.values(): + if library_id and str(cached.get("LibraryId") or cached.get("ParentId") or "") != str(library_id): + continue + if isinstance(cached.get("Genres"), list): + agg.update({g for g in cached["Genres"] if g}) + else: + agg.update({gi.get("Name") for gi in (cached.get("GenreItems") or []) if gi.get("Name")}) + if agg: + return sorted(agg) + + url = f"{self.emby_server_url}/emby/Genres?Recursive=true&ParentId={library_id}&api_key={self.api_key}" + data = requests.get(url, headers=self.headers).json() + return sorted({it.get("Name") for it in data.get("Items", []) if it.get("Name")}) + + def _resolve_item_id_safe(self, plex_object): + """Nutzt deine Helferfunktion, wenn vorhanden; einfacher Fallback sonst.""" + if hasattr(plex_object, "ratingKey"): + return plex_object.ratingKey + elif isinstance(plex_object, int): + return str(plex_object) + elif isinstance(plex_object, str): + return str(plex_object) + + return None + # if isinstance(plex_object, Plex): + # return None # bedeutet: Library-weit arbeiten + # raise WARNING(f"Emby item object is not configured {plex_object!r}") + + def _studios_from_item_cached(self,item: dict) -> list[str]: + """Extrahiert Studio-Namen robust aus einem gecachten Emby-Item.""" + studios = item.get("Studios") or [] + out = [] + if isinstance(studios, list): + for s in studios: + if isinstance(s, dict): + n = s.get("Name") or s.get("name") + if n: out.append(str(n).strip()) + elif isinstance(s, str): + n = s.strip() + if n: out.append(n) + elif isinstance(studios, str): + out.extend([p.strip() for p in studios.split(",") if p.strip()]) + return out + + def get_emby_studios(self, plex_object, library_id) -> list[str]: + """ + Studios für ein Item (oder – wenn kein Item angegeben – für die ganze Library). + Primär aus self.item_cache; API-Fallback nur wenn nötig. + """ + studios = set() + item_id = self._resolve_item_id_safe(plex_object) + + # 1) Item-spezifisch + if item_id: + item = self.item_cache.get(str(item_id)) or self.get_item(item_id) + if not item: + return [] + for s in self._studios_from_item_cached(item): + # TODO: Wenn du später Networks via Unicode '📡' trennen willst, + # und Studios OHNE dieses Präfix behalten möchtest: + # if s.startswith('📡'): + # continue + studios.add(s) + return sorted(studios) + + # 2) Aggregiert aus Cache (optional nach Library gefiltert) + for it in self.item_cache.values(): + if library_id and str(it.get("LibraryId") or it.get("ParentId") or "") != str(library_id): + continue + for s in self._studios_from_item_cached(it): + # TODO: Unicode-Trennung siehe oben: + # if s.startswith('📡'): + # continue + studios.add(s) + + if studios: + return sorted(studios) + + # 3) Fallback: Studios-Endpunkt + url = f"{self.emby_server_url}/emby/Studios?Recursive=true&ParentId={library_id}&api_key={self.api_key}" + response = requests.get(url, headers=self.headers) + if response.status_code != 200: + raise Failed( + f"Failed to retrieve studios for library {library_id}. " + f"Response: {response.status_code} - {response.text}" + ) + + for item in response.json().get("Items", []): + name = str(item.get("Name", "")).strip() + if not name: + continue + # TODO: Unicode-Trennung (Studios ohne '📡'): + # if name.startswith('📡'): + # continue + studios.add(name) + + return sorted(studios) + + def get_emby_networks(self, plex_object, library_id: str): + """ + Gibt 'Networks' zurück – aktuell identisch zu Emby-Studios (später per Unicode trennbar). + Nutzt primär den Runtime-Cache (self.item_cache); API-Fallback nur wenn nötig. + """ + nets = set() + item_id = self._resolve_item_id_safe(plex_object) + + # 1) Item-spezifisch + if item_id: + item = self.item_cache.get(str(item_id)) or self.get_item(item_id) + if not item: + return [] + for n in self._studios_from_item_cached(item): + # TODO: Unicode-Trennung aktivieren, wenn du echte Networks separierst: + # if not n.startswith('📡'): + # continue + # n = n[2:] + nets.add(n) + return sorted(nets) + + # 2) Aggregiert aus Cache (optional nach Library gefiltert) + for it in self.item_cache.values(): + if library_id and str(it.get("LibraryId") or it.get("ParentId") or "") != str(library_id): + continue + for n in self._studios_from_item_cached(it): + # TODO: Unicode-Trennung aktivieren, wenn du echte Networks separierst: + # if not n.startswith('📡'): + # continue + # n = n[2:] + nets.add(n) + + if nets: + return sorted(nets) + + # 3) Fallback auf Studios-Endpunkt, falls Cache leer + url = f"{self.emby_server_url}/emby/Studios?Recursive=true&ParentId={library_id}&api_key={self.api_key}" + response = requests.get(url, headers=self.headers) + if response.status_code != 200: + raise Failed(f"Failed to retrieve studios for library {library_id}. " + f"Response: {response.status_code} - {response.text}") + for item in response.json().get("Items", []): + name = str(item.get("Name", "")).strip() + if not name: + continue + # TODO: Unicode-Trennung aktivieren, wenn du echte Networks separierst: + # if not name.startswith('📡'): + # continue + # name = name[2:] + nets.add(name) + + return sorted(nets) + + def get_library_studios(self, name: str | list[str] | None = None) -> list[str]: + """ + Studios der aktuellen Library. + - Primär aus self.item_cache (schnell), sonst API-Fallback. + - Wenn 'name' gesetzt ist, wird die Schnittmenge (Treffer) zurückgegeben. + """ + if not self.studio_list: + studios = set() + # 1) Aus Cache + for it in self.item_cache.values(): + if str(it.get("LibraryId") or it.get("ParentId") or "") != str(self.library_id): + continue + for s in self._studios_from_item_cached(it): + # Hinweis: Hier KEINE Trennung – dies sind „Studios“, + # Unicode-Filter wäre nur für „Networks“ sinnvoll: + # if s.startswith('📡'): + # continue + studios.add(s) + + # 2) Fallback API + if not studios: + url = f"{self.emby_server_url}/emby/Studios?ParentId={self.library_id}&api_key={self.api_key}" + response = requests.get(url, headers=self.headers) + if response.status_code != 200: + raise Failed(f"Failed to retrieve studios for library {self.library_id}. " + f"Response: {response.status_code} - {response.text}") + for item in response.json().get("Items", []): + n = str(item.get("Name", "")).strip() + if n: + # Unicode-Logik hier bewusst auskommentiert (nur Networks): + # if n.startswith('📡'): + # continue + studios.add(n) + + self.studio_list = sorted(studios) + + # Filter nach 'name' (validiere und gib nur gefundene zurück) + if name: + haystack = set(self.studio_list) + if isinstance(name, str): + wanted = {p.strip() for p in name.split(",") if p.strip()} + else: + wanted = {str(p).strip() for p in name if str(p).strip()} + hits = sorted(haystack & wanted) + return hits + + return self.studio_list + + def multiEditRatings(self, rating_edits): + """ + Bearbeitet die Bewertungen für die übergebenen Item-IDs. + Jede Item-ID wird nur einmal bearbeitet, und fehlende Bewertungen werden mit leeren Feldern behandelt. + + :param rating_edits: Ein Dictionary mit Bewertungen (audienceRating, rating, userRating) und zugehörigen Item-IDs. + """ + # Sammele alle zu bearbeitenden Items + update_items = {} + + for field_attr, edits in rating_edits.items(): + for rating_value, item_ids in edits.items(): + for item_id in item_ids: + # Initialisiere das Dictionary für diese Item-ID, falls es noch nicht existiert + if item_id not in update_items: + update_items[item_id] = {} + + # Aktualisiere das entsprechende Feld basierend auf `field_attr` + if field_attr == "audienceRating": + update_items[item_id]["CommunityRating"] = rating_value + elif field_attr == "rating": + update_items[item_id]["CriticRating"] = float(rating_value) * 10 if rating_value else '' + elif field_attr == "userRating": + update_items[item_id]["CustomRating"] = rating_value + + # Wende die Updates an + for item_id, rating_data in update_items.items(): + # Führe das Update für das Item aus + # print(".", end="", flush=True) + updates = {} + if "CommunityRating" in rating_data: + updates["CommunityRating"] = rating_data["CommunityRating"] + if "CriticRating" in rating_data: + updates["CriticRating"] = float(rating_data["CriticRating"])*10 + if "CustomRating" in rating_data: + updates["CustomRating"] = rating_data["CustomRating"] + + self.__update_item( + item_id, + updates + ) + return update_items + + # ToDo: Implementation + def multiEditField(self, rating_keys,field_attr=None, new_value=None, locked=False): + # raise Warning(f"multiEditField not implemented for {rating_keys} - {field_attr} - {new_value} {locked}") + # return + + self._ensure_http_session() + + + for item in rating_keys: + item_id = item.ratingKey + changes ={} + if field_attr == "audienceRating": + changes["CommunityRating"] = new_value + item.audienceRating = new_value + elif field_attr == "rating": + changes["CriticRating"] = float(new_value)*10 + item.rating = new_value + # self.__update_item(item_id,{"CriticRating": new_value}) + elif field_attr == "userRating": # ToDo: use ProviderId instead of age rating + changes["CustomRating"] = new_value + item.userRating = new_value + elif field_attr == "contentRating": + changes["OfficialRating"] = new_value + item.contentRating = new_value # item update still needed? + elif field_attr == "studio": + # id = self.get + + my_url = f"{self.emby_server_url}/emby/Studios/{new_value}?api_key={self.api_key}" + + # # Make a request to get the Emby item details + response = self._http_session.get(my_url) + + # response = requests.get( + # f"{self.emby_server_url}/emby/Tags?{ids}Recursive=true&{library_id}api_key={self.api_key}'" + # ) + + # tags empty for collections + if response.status_code != 200: + studio_id = new_value + else: + item_details = response.json() + studio_id = item_details.get('Id') + + + changes["Studios"] = {"Name":new_value, "Id": studio_id} + item.studio = new_value + # self.__update_item(item_id,{"CustomRating": new_value}) + self.__update_item(item_id,changes) + + if field_attr in ["audienceRating","rating","userRating","studio","contentRating"]: + # CustomRating ? + return + + raise Warning(f"multiEditField not implemented for {rating_keys} - {field_attr} - {new_value} {locked}") + + def multi_edit(self,rating_keys, **kwargs): + raise Warning(f"multi_edit not implemented for {rating_keys} - {kwargs}") + + # raise Warning(f"multi_edit not implemented for {new_studio}") + + def editItemTitle(self, ratingKey, new_title): + raise Warning(f"editItemTitle not implemented for {ratingKey} - {new_title}") + + # ==== Session einmalig bereitstellen (falls nicht vorhanden) ==== + # ==== HTTP-Session mit Retry/Backoff (einmalig in __init__ oder lazy) ==== + def _ensure_http_session(self): + if self.session: + return + import requests + from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry + + self.session = requests.Session() + retries = Retry( + total=5, + backoff_factor=0.5, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=frozenset(["GET", "POST", "PUT", "PATCH"]) + ) + adapter = HTTPAdapter(max_retries=retries, pool_connections=10, pool_maxsize=20) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # ==== Leichte Normalisierung statt DeepDiff ==== + def _normalize_people_for_compare(self, people_list): + """ + Vergleicht nur die Felder, die für die Gleichheit relevant sind – in Ordnung (Order zählt). + Ignoriert 'PrimaryImageTag' und sonstige irrelevante Felder. + """ + norm = [] + for p in people_list or []: + norm.append(( + str(p.get("Id") if p.get("Id") is not None else p.get("Name", "")).strip().casefold(), + (p.get("Name") or "").strip().casefold(), + p.get("Type") or "", + (p.get("Role") or "").strip() + )) + return norm + + # ==== zentraler Personen-Index je Library+Rolle ==== + def _build_people_index(self, role: str): + """ + Baut (und cached) einen Index für die Komplettriege der Personen einer Library+Rolle. + Maps: + - by_id[str(Id)] -> person-dict + - by_name[name.casefold()] -> list[person-dict] (für exakte Namensmatches) + - by_provider[(prov_case, tmdb_id_str)] -> person-dict + """ + + key = f"{role}" + if key in self.people_index: + return self.people_index[key] + + # Vollständige Liste einmal ziehen (mit Session & Keep-Alive) + self._ensure_http_session() + base_url = self.emby_server_url + "/emby/Persons" + params = { + # "ParentId": library_id, + "PersonTypes": role, + "Fields": "ProviderIds,ImageTags", + "api_key": self.api_key, + } + try: + r = self._http_session.get(base_url, headers=self.headers, params=params, timeout=60) + r.raise_for_status() + items = r.json().get("Items", []) + except Exception: + items = [] + + by_id, by_name, by_provider = {}, {}, {} + for it in items: + pid = it.get("Id") + if pid: + by_id[str(pid)] = it + name = (it.get("Name") or "").strip() + if name: + by_name.setdefault(name.casefold(), []).append(it) + prov = it.get("ProviderIds") or {} + for k, v in prov.items(): + if v is None: + continue + by_provider[(str(k).casefold(), str(v))] = it + + idx = {"by_id": by_id, "by_name": by_name, "by_provider": by_provider, "all": items} + self.people_index[key] = idx + return idx + + # ==== get_people: nutzt Index & gezielte Suche, aber macht keine Duplikat-Requests mehr ==== + def get_people(self, role: str, person_list=None): + """ + - Ohne person_list: komplette (gecachte) Liste via Index (ein Request pro Library+Rolle). + - Mit person_list: exakte Namensmatches aus dem Index, fällt bei Bedarf auf minimale Such-Requests zurück + (nur wenn wirklich keine Namens-Treffer im Index existieren). + Cached zusätzlich: self.people_lib_cache[term.lower()] + """ + + idx = self._build_people_index(role) + + # Vollständige Liste + if not person_list: + return idx["all"] + + results_by_id = {} + for term in person_list: + if not term: + continue + search = str(term).strip() + if not search: + continue + ck = search.lower() + + # 1) Cache für gezielte Suchliste? + if ck in self.people_lib_cache: + for it in self.people_lib_cache[ck]: + if it and it.get("Id"): + results_by_id[it["Id"]] = it + continue + + # 2) Exaktes Namensmatch aus dem Index (0 Requests) + exact = idx["by_name"].get(ck, []) + if exact: + ordered = exact # exakte Treffer bevorzugen + self.people_lib_cache[ck] = ordered + for it in ordered: + if it and it.get("Id"): + results_by_id[it["Id"]] = it + continue + + # 3) Fallback: einmalige Emby-Suche mit SearchTerm + self._ensure_http_session() + base_url = self.emby_server_url + "/emby/Persons" + params = { + "PersonTypes": role, + "Fields": "ProviderIds,ImageTags", + "SearchTerm": search, + "api_key": self.api_key, + } + try: + r = self._http_session.get(base_url, headers=self.headers, params=params, timeout=20) + r.raise_for_status() + items = r.json().get("Items", []) + except Exception: + items = [] + + # Exakte Namens-Treffer bevorzugen + exact = [p for p in items if (p.get("Name") or "").strip().lower() == ck] + ordered = exact or items + + # lokalen Index NICHT überschreiben; aber Suchcache füllen + self.people_lib_cache[ck] = ordered + for it in ordered: + if it and it.get("Id"): + results_by_id[it["Id"]] = it + + return list(results_by_id.values()) + + def get_person_by_name(self, person_name): + """ + - Ohne person_list: komplette (gecachte) Liste via Index (ein Request pro Library+Rolle). + - Mit person_list: exakte Namensmatches aus dem Index, fällt bei Bedarf auf minimale Such-Requests zurück + (nur wenn wirklich keine Namens-Treffer im Index existieren). + Cached zusätzlich: self.people_lib_cache[term.lower()] + """ + + # 3) Fallback: einmalige Emby-Suche mit SearchTerm + self._ensure_http_session() + base_url = self.emby_server_url + "/emby/Persons" + params = { + "SearchTerm": person_name, + "Fields": "ProviderIds,ImageTags", + "api_key": self.api_key, + } + try: + r = self._http_session.get(base_url, headers=self.headers, params=params, timeout=20) + r.raise_for_status() + items = r.json().get("Items", []) + except Exception: + items = [] + + # Exakte Namens-Treffer bevorzugen + + return items + + def _get_person_by_name_cached(self, clean_name: str): + key = (clean_name or "").strip() + hit = self.cached_person_names.get(key) + if hit is None: + try: + hit = self.get_person_by_name(key) or [] + except Exception: + hit = [] + self.cached_person_names[key] = hit + # if len(self.cached_person_names) > 200_000: + # self.cached_person_names.clear() + return hit + + def get_person_info_via_library(self, role: str, provider: str, tmdb_id: int | str, + person_name: str): + """ + STRIKTE Disambiguierung für Massenlauf: + 1) exakte Provider-ID im Index? -> sofortiger Treffer + 2) exakter Name im Index und EIN eindeutiger Kandidat -> Treffer + 3) mehrere gleichnamige Kandidaten: + - behalte nur Kandidaten OHNE abweichende ProviderId (falls vorhanden) + - wenn danach EIN Kandidat übrig bleibt -> Treffer + - sonst: KEIN Auto-Link (Platzhalter verwenden) + Liefert (emby_person_id, linked_bool[=müsste ProviderId gesetzt/ergänzt werden]) oder (None, False). + """ + prov = (provider or "Tmdb") + prov_key = prov.casefold() + tmdb_id_str = str(tmdb_id) + cache_key = f"{prov}-{tmdb_id_str}" + + def _cand_has_provider(person: dict, prov_key: str, ext_id: str) -> bool: + pids = person.get("ProviderIds") or {} + return (pids.get(prov_key) == ext_id) or (pids.get(prov_key.capitalize()) == ext_id) + + def _dedupe_by_id(items): + seen, out = set(), [] + for it in items or []: + pid = it.get("Id") + if pid and pid not in seen: + seen.add(pid); + out.append(it) + return out + + if str(tmdb_id) == "64796": + pass + + # schneller Pos/Neg-Cache + if cache_key in self.people_cache: + emby_id = self.people_cache[cache_key] + return (emby_id, False) if emby_id else (None, False) + + # Index für die Rolle laden + idx = self._build_people_index(role) + + # 1) Provider-First (perfekt disambiguiert) + byprov = idx["by_provider"].get((prov_key, tmdb_id_str)) + if byprov and byprov.get("Id"): + self.people_cache[cache_key] = byprov["Id"] + # Provider ist schon korrekt -> kein Änderungsbedarf + return byprov["Id"], False + + # 2) Exakter Namensmatch (nur eindeutige Treffer akzeptieren) + name_l = (person_name or "").strip().casefold() + candidates = _dedupe_by_id(idx["by_name"].get(name_l, [])) + + if len(candidates) == 1: + # genau ein Kandidat -> ok + person = candidates[0] + person_id = person.get("Id") + if not person_id: + self.people_cache[cache_key] = None + return None, False + + # Prüfe, ob ProviderIds ergänzt werden müssten + will_change = not _cand_has_provider(person, prov_key, tmdb_id_str) + self.people_cache[cache_key] = person_id + return person_id, will_change + + if len(candidates) > 1: + # 3) Mehrdeutigkeit entschärfen: + # Schließe Kandidaten aus, die bereits eine ABWEICHENDE ProviderId für diesen Provider tragen. + filtered = [] + for p in candidates: + pids = p.get("ProviderIds") or {} + # Wenn es bereits eine Tmdb-Id gibt, die NICHT zu tmdb_id_str passt -> Kandidat verwerfen + if prov in pids or prov_key in pids: + if _cand_has_provider(p, prov_key, tmdb_id_str): + filtered.append(p) # passt exakt + # sonst raus + else: + # keine Tmdb verknüpft -> neutral (behalten) + filtered.append(p) + + filtered = _dedupe_by_id(filtered) + + if len(filtered) == 1: + person = filtered[0] + person_id = person.get("Id") + will_change = not _cand_has_provider(person, prov_key, tmdb_id_str) + self.people_cache[cache_key] = person_id + return person_id, will_change + + # weiterhin mehrdeutig -> sicher bleiben, NICHT auto-linken + self.people_cache[cache_key] = None + return None, False + + # 4) Gar kein Namensmatch -> kein Auto-Link + self.people_cache[cache_key] = None + return None, False + + def get_person_info_bulk(self, tmdb_ids: list, provider: str = "tmdb", chunk_size: int = 200): + """ + Liefert dict[tmdb_id:int] -> emby_person_id:str. + - nutzt self.session (Connection-Pooling) + - bewertet Dupe-Sets nur pro Batch (kein Mehrfach-Demote) + - wählt die kleinste Emby-ID als kanonisch + - demote pro falscher PID nur einmal (idempotent) + - behält Dupe-Listen für Logging/Persistenz bei + """ + if not tmdb_ids: + return {} + + + # IDs normalisieren + try: + norm_ids = sorted({int(x) for x in tmdb_ids if x is not None and str(x).strip() != ""}) + except ValueError: + norm_ids = sorted({int(x) for x in tmdb_ids if str(x).isdigit()}) + if not norm_ids: + return {} + + self._ensure_http_session() + result: dict[int, str] = {} + + base_url = f"{self.emby_server_url}/emby/Users/{self.user_id}/Items" + + # In Batches abfragen und JE Batch separat bewerten/demoten + for i in range(0, len(norm_ids), chunk_size): + batch = norm_ids[i:i + chunk_size] + provider_ids = ",".join(f"{provider}.{pid}" for pid in batch) + + params = { + "Recursive": "true", + "Fields": "ProviderIds", + "IncludeItemTypes": "Person", + "AnyProviderIdEquals": provider_ids, + "api_key": self.api_key, + } + + try: + resp = self.session.get(base_url, headers=self.headers, params=params, timeout=20) + resp.raise_for_status() + payload = resp.json() or {} + except Exception as e: + try: + logger.warning(f"[get_person_info_bulk] Request-Fehler (Batch {i // chunk_size + 1}): {e}") + except Exception: + pass + continue + + items = (payload.get("Items") or payload.get("items") or []) or [] + + # Batch-lokale Sammelstrukturen + tmdb_to_ids: dict[int, set[str]] = {} + tid_to_name: dict[int, str] = {} + + # Treffer sammeln + for item in items: + emby_id = item.get("Id") + name = (item.get("Name") or "").strip() + prov = item.get("ProviderIds") or {} + + # Provider-ID robust lesen (tmdb/Tmdb/TMDb) + tmdb_id_raw = ( + prov.get(provider.lower()) + or prov.get(provider.capitalize()) + or prov.get("TMDb") + ) + if tmdb_id_raw is None: + continue + + try: + tmdb_id = int(tmdb_id_raw) + except (TypeError, ValueError): + continue + + s = tmdb_to_ids.setdefault(tmdb_id, set()) + s.add(str(emby_id)) + + if name: + prev = tid_to_name.get(tmdb_id) + if prev is None or (prev == "John Doe" and name != "John Doe"): + tid_to_name[tmdb_id] = name + + # Dupe-Bewertung NUR für diesen Batch + for tid, idset in tmdb_to_ids.items(): + ids_sorted = sorted({str(x) for x in idset}, key=lambda x: int(x)) # numerische Sortierung + chosen = ids_sorted[-1] + result[tid] = chosen + + if len(ids_sorted) > 1: + # Dupe-Infos für Logs/Persistenz + try: + tid_int = int(tid) + except Exception: + tid_int = tid + # self._person_dupes_last[tid_int] = ids_sorted + self._person_dupes_choice_last[tid_int] = chosen + + # Persistentes Mapping (gewollt hart abbrechen auf Fehler) + try: + self.config.Cache.update_tmdb_person_map( + expired=False, + tmdb_id=int(tid), + emby_id=str(chosen), + expiration=self.config.Cache.expiration + ) + except Exception: + raise Failed + pass # bewusst beibehalten + + # Nicht-kanonische Personen demoten (pro PID nur einmal) + wrong_ids = ids_sorted[:-1] # alle außer dem letzten + for wrong_pid in wrong_ids: + if wrong_pid in self._person_already_demoted: + continue + try: + self._demote_duplicate_person(wrong_pid) + self._person_already_demoted.append(wrong_pid) + try: + logger.info(f"[get_person_info_bulk] Demoted person Emby id {wrong_pid}") + except Exception: + pass + except Exception as e: + try: + logger.warning(f"[get_person_info_bulk] demote failed for pid={wrong_pid}: {e}") + except Exception: + pass + + # Zusammenfassung loggen + pname = tid_to_name.get(tid) + label = f"{tid} - {pname}" if pname else f"{tid}" + try: + logger.warning( + f"[get_person_info_bulk] TMDb-Person-ID {label} ist mehreren Emby-IDs zugeordnet: " + f"{ids_sorted}. Verwende {chosen}; demoted {wrong_ids}" + ) + except Exception: + pass + + return result + + def _strip_alias_suffix(self, name: str, tmdb_id) -> str: + s = (name or "").strip() + if not s or not s.endswith(tmdb_id): + return s + while s.endswith(tmdb_id): + s = s[:-len(tmdb_id)] + return s.strip() + + def _norm_name(self, s: str) -> str: + import unicodedata + s = unicodedata.normalize("NFKD", s or "") + s = "".join(c for c in s if not unicodedata.combining(c)) + return _re.sub(r"\s+", " ", s).strip().casefold() + + def _attach_tmdb_to_person(self, emby_person_id: str, tmdb_id: int) -> None: + if not (emby_person_id and tmdb_id): + return + self._ensure_http_session() + url = f"{self.emby_server_url}/emby/Items/RemoteSearch/Apply/{emby_person_id}" + params = {"api_key": self.api_key} + data = {"ProviderIds": {"tmdb": str(tmdb_id)}} + try: + self.session.post(url, headers=self.headers, params=params, json=data, timeout=20) + except Exception: + pass + + def _demote_duplicate_person(self, emby_person_id: str) -> None: + return + try: + logger.info(f"Cleared person {emby_person_id} and renamed to 'John Doe'") + + self.update_item(emby_person_id, {"Id": emby_person_id, "Name": "John Doe", "ProviderIds": {}}) + except Exception: + pass + + # ==== build_emby_people_from_tmdb: unverändert in Logik, aber ohne unnötige Requests ==== + def build_emby_people_from_tmdb(self, my_cast, my_crew, provider: str = "tmdb"): + """ + Baut die gewünschte Emby-Personenliste (Cast + Crew) auf Basis von TMDb-Daten. + Nutzt Bulk-Lookup gegen Emby (self.get_person_info_bulk) und aktualisiert einen Cache: + self.cached_tmdb_ids: dict[int -> str] (tmdb_id -> emby_id) + self.missing_tmdb_ids: set[int] (tmdb_ids, die in Emby nicht existieren) + """ + + # ------- Job-Mapping (Type + optionale Crew-Role) ------- + # Schlüssel immer lower-case! + job_to_type_role = { + # Director + "director": ("Director", None), + + # Writers + "writer": ("Writer", "Autor"), + "screenplay": ("Writer", None), + "screenwriter": ("Writer", None), + "teleplay": ("Writer", None), + "author": ("Writer", "Autor"), + "novel": ("Writer", "Romanvorlage"), + "adaptation": ("Writer", "Adaption"), + "story": ("Writer", "Story"), + "comic book": ("Writer", "Comicvorlage"), + "original story": ("Writer", "Story"), + "theatre play": ("Writer", "Theaterstück"), + + # Music + "composer": ("Composer", None), + "original music composer": ("Composer", "Filmmusik"), + "lyricist": ("Lyricist", None), + "songs": ("Lyricist", "Songs"), # dein Beispiel + "theme song performance": ("Lyricist", "Theme Song"), # dein Beispiel + + # Other crew types you already support + "producer": ("Producer", None), + "conductor": ("Conductor", None), + } + allowed_jobs = set(job_to_type_role.keys()) + + def map_job(job: str): + jl = (job or "").strip().lower() + return job_to_type_role.get(jl) # -> (etype, role) oder None + + # ---------- Dedup-Key jetzt inkl. Role ---------- + def key_for(emby_id, name, etype, role): + # bevorzugt ID, sonst Name (casefold). Role ist Teil der Identität. + return (("id", str(emby_id)) if emby_id else ("name", (name or "").casefold()), etype, (role or None)) + + per_key = {} + ordered_cast = [] + crew_buckets = {"Director": [], "Writer": [], "Producer": [], "Composer": [], "Conductor": [], "Lyricist": []} + + # --- Robuste Filter --- + cast_filtered = [ + a for a in (my_cast or []) + if not _re.search(r"\buncredited\b", (a.get("character") or ""), flags=_re.IGNORECASE) + ] + crew_filtered = [ + m for m in (my_crew or []) + if (m.get("job") and (m.get("job").strip().lower() in allowed_jobs)) + ] + + # --- IDs sammeln (aus bereits gecachten Listen) --- + tmdb_ids = [c.get("person_id") for c in (cast_filtered + crew_filtered) if c.get("person_id")] + tmdb_ids = [int(t) for t in tmdb_ids if str(t).isdigit()] + + # --- 1) DB-Cache vorwärmen: emby_id + alias --- + db_map, db_missing, db_expired = ( + self.config.Cache.query_tmdb_person_map_bulk(tmdb_ids, 3) + if self.config and self.config.Cache else ({}, set(tmdb_ids), set()) + ) + # for tid, rec in db_map.items(): + # l = rec.get("emby_id") + # if l and tid not in self.cached_tmdb_ids.keys() and tid not in db_expired: + # self.cached_tmdb_ids[tid] = rec["emby_id"] + + # --- Cache-Validierung nur für die in diesem Lauf benötigten Mappings --- + def _validate_cached_person_mappings_local(t2e: dict): + if not t2e: + return set() + emby_ids = sorted({str(e) for e in t2e.values() if e}) + items = [] + CHUNK = 100 + for i in range(0, len(emby_ids), CHUNK): + chunk = ",".join(emby_ids[i:i + CHUNK]) + endpoint = f"/emby/Items?Ids={chunk}&Fields=ProviderIds,Type,Id&api_key={self.api_key}" + url = self.emby_server_url + endpoint + try: + r = requests.get(url, headers=self.headers, timeout=10) + data = r.json() or {} + items.extend(data.get("Items") or data.get("items") or []) + except Exception as e: + try: + logger.error(e) + except Exception: + pass + by_id = {str(it.get("Id")): it for it in items if it and it.get("Id") is not None} + stale = set() + provider_key = "Tmdb" if (provider or "").lower() == "tmdb" else provider + for tid, eid in t2e.items(): + it = by_id.get(str(eid)) + if not it or it.get("Type") != "Person": + stale.add(tid); + continue + prov = it.get("ProviderIds") or {} + v1 = str(prov.get(provider_key)) + v2 = str(prov.get(provider_key + "Id")) + if str(tid) not in {v1, v2}: + stale.add(tid) + for tid in stale: + self.cached_tmdb_ids.pop(tid, None) + try: + if self.config and self.config.Cache: + self.config.Cache.update_tmdb_person_map( + expired=True, tmdb_id=int(tid), emby_id=None, expiration=self.config.Cache.expiration + ) + except Exception: + pass + return stale + + needed_cached_map = {tid: self.cached_tmdb_ids.get(tid) for tid in tmdb_ids if tid in self.cached_tmdb_ids} + stale_due_validation = _validate_cached_person_mappings_local(needed_cached_map) + + # --- 2) Nur fehlende/abgelaufene gegen Emby auflösen --- + to_resolve = sorted({ + tid for tid in tmdb_ids + if (tid not in self.cached_tmdb_ids) or (tid in db_expired) + }) + to_resolve = sorted(set(to_resolve) | set(stale_due_validation)) + # to_resolve = tmdb_ids + resolved = {} + if to_resolve: + try: + resolved = self.get_person_info_bulk(to_resolve, provider=provider) # {tmdb_id:int -> emby_id:str} + except Exception as e: + logger.error(e) + resolved = {} + if resolved: + self.cached_tmdb_ids.update({int(k): str(v) for k, v in resolved.items()}) + # === Aktuelle Emby-Namen prefetchen === + needed_emby_ids =[] + for my_tmdb_id in tmdb_ids: + if int(my_tmdb_id) in self.cached_tmdb_ids.keys(): + needed_emby_ids.append(self.cached_tmdb_ids.get(int(my_tmdb_id), "")) + # known_emby_ids = list({str(v) for v in self.cached_tmdb_ids.values()}) + emby_name_by_id = {} + if needed_emby_ids: + try: + id_to_item = self.get_items_bulk(needed_emby_ids) + except Exception: + id_to_item = {} + for pid, item in (id_to_item or {}).items(): + nm = item.get("Name") + if nm: + emby_name_by_id[str(pid)] = nm + pass + # Map TMDb -> aktueller Emby-Name (falls vorhanden) + # emby_name_by_tid = {} + # for tid, pid in self.cached_tmdb_ids.items(): + # nm = emby_name_by_id.get(str(pid)) + # if nm: + # try: + # # emby_name_by_tid[int(tid)] = nm + # # except Exception: + # pass + + # ================= + # ----- CAST ------ + # ================= + for c in (cast_filtered or []): + tmdb_id = c.get("person_id") + name = c.get("name") + role = c.get("character") or None + if not tmdb_id or not name: + continue + + try: + tmdb_id_int = int(tmdb_id) + except Exception: + tmdb_id_int = None + + emby_id = self.cached_tmdb_ids.get(tmdb_id_int if tmdb_id_int is not None else tmdb_id) + + # Stale-Guard: wenn diese Emby-ID nicht in den eben geprefetchten Items vorkommt, + # behandle sie als "nicht auflösbar". + if emby_id and str(emby_id) not in emby_name_by_id: + logger.warning(f"[build_emby_people_from_tmdb] drop stale emby_id={emby_id} for tmdb={tmdb_id}") + emby_id = None + + # Emby-ID bekannt? → aktuellen Emby-Namen verwenden + # if emby_id: + # if not "John Doe" in emby_name_by_id.get(str(emby_id)): + # entry_name = emby_name_by_id.get(str(emby_id)) or name + # try: + # self.ensure_person_latin_name(str(emby_id), + # int(tmdb_id_int if tmdb_id_int is not None else tmdb_id), + # entry_name) + # except Exception: + # entry_name = name + # pass + # else: + entry_name = name + + if not emby_id: + # Platzhalter deduplizieren (inkl. Role) + k_name = key_for(None, entry_name, "Actor", role) + if k_name not in per_key: + entry = {"Id": entry_name, "Name": entry_name, "Type": "Actor", "Tmdb": tmdb_id} + if role: + entry["Role"] = role + per_key[k_name] = entry + ordered_cast.append(entry) + else: + if role and "Role" not in per_key[k_name]: + per_key[k_name]["Role"] = role + continue + + # Emby-ID vorhanden → über ID deduplizieren (inkl. Role) + k_id = key_for(emby_id, entry_name, "Actor", role) + k_name = key_for(None, entry_name, "Actor", role) + if k_name in per_key: + entry = per_key.pop(k_name) + entry["Id"] = str(emby_id) + entry["Name"] = entry_name + entry["Tmdb"] = tmdb_id + per_key[k_id] = entry + elif k_id not in per_key: + entry = {"Id": str(emby_id), "Name": entry_name, "Type": "Actor", "Tmdb": tmdb_id} + if role: + entry["Role"] = role + per_key[k_id] = entry + ordered_cast.append(entry) + else: + if role and "Role" not in per_key[k_id]: + per_key[k_id]["Role"] = role + + # ================= + # ----- CREW ------ + # ================= + for m in (crew_filtered or []): + tmdb_id = m.get("person_id") + name = m.get("name") + mapped = map_job(m.get("job")) + if not (tmdb_id and name and mapped): + continue + + etype, crew_role = mapped # z.B. ("Writer", "Screenplay") oder ("Writer", None) + + try: + tmdb_id_int = int(tmdb_id) + except Exception: + tmdb_id_int = None + + emby_id = self.cached_tmdb_ids.get(tmdb_id_int if tmdb_id_int is not None else tmdb_id) + # Stale-Guard: wenn diese Emby-ID nicht in den eben geprefetchten Items vorkommt, + # behandle sie als "nicht auflösbar". + if emby_id and str(emby_id) not in emby_name_by_id: + logger.warning(f"[build_emby_people_from_tmdb] drop stale emby_id={emby_id} for tmdb={tmdb_id}") + emby_id = None + + entry_name = name + + if not emby_id: + k_name = key_for(None, entry_name, etype, crew_role) + if k_name not in per_key: + entry = {"Id": entry_name, "Name": entry_name, "Type": etype, "Tmdb": tmdb_id} + if crew_role: + entry["Role"] = crew_role + per_key[k_name] = entry + crew_buckets[etype].append(entry) + continue + + k_id = key_for(emby_id, entry_name, etype, crew_role) + k_name = key_for(None, entry_name, etype, crew_role) + if k_name in per_key: + entry = per_key.pop(k_name) + entry["Id"] = str(emby_id) + entry["Name"] = entry_name + entry["Tmdb"] = tmdb_id + if crew_role: + entry["Role"] = crew_role + per_key[k_id] = entry + elif k_id not in per_key: + entry = {"Id": str(emby_id), "Name": entry_name, "Type": etype, "Tmdb": tmdb_id} + if crew_role: + entry["Role"] = crew_role + per_key[k_id] = entry + crew_buckets[etype].append(entry) + + desired = [] + desired.extend(ordered_cast) + for t in ("Director", "Writer", "Producer", "Composer", "Conductor", "Lyricist"): + desired.extend(crew_buckets[t]) + + return desired + + # --- Helfer: Akzente entfernen (für SortName o.ä.) --- + def _strip_accents(self, s: str) -> str: + if not s: + return s + return "".join(ch for ch in unicodedata.normalize("NFKD", s) if not unicodedata.combining(ch)) + + def _compute_sort_name(self, name: str) -> str: + # einfache, robuste Sortierform: akzentfrei + return self._strip_accents(name or "").strip() + + # --- Helfer: Vergleichssignatur ohne Name (nur Id/Type/Role & Reihenfolge) --- + def _people_signature_no_name(self, people_list): + return [ + ( + str(p.get("Id") or ""), + p.get("Type") or "", + (p.get("Role") or None) + ) + for p in (people_list or []) + ] + + # --- Helfer: Finde reine Namens-Abweichungen (mapping über Id) --- + def _collect_person_name_fixes(self, current_people, desired_people): + desired_by_id = { + str(p.get("Id")): (p.get("Name") or "") + for p in (desired_people or []) + if p.get("Id") is not None + } + fixes = [] # [(person_id:str, desired_name:str)] + for p in (current_people or []): + pid = str(p.get("Id") or "") + if not pid or pid not in desired_by_id: + continue + cur_name = p.get("Name") or "" + des_name = desired_by_id[pid] + if des_name and cur_name != des_name: + fixes.append((pid, cur_name, des_name)) + return fixes + + # --- Helfer: Person direkt korrigieren (verwendet deine update_item) --- + def _update_person_name_if_needed(self, person_id: str, desired_name: str): + # Optional: einmal pro Lauf fixen + if person_id in self._person_name_fix_cache: + return None + + payload = { + "Name": desired_name, + # sinnvolle Sortierung ohne Akzente + "ForcedSortName": self._compute_sort_name(desired_name), + } + resp = self.update_item(person_id, payload) + self._person_name_fix_cache.add(person_id) + return resp + + @property + def items_cache(self): + """ + Lazy-initialized Item-Cache. + - _items_cache: id -> zuletzt gemergtes Item-Dict + - _items_cache_fields: id -> Set[str] der vorhandenen Fields + - _items_cache_ts: id -> float (Unix-Zeitpunkt des letzten Refresh) + - items_cache_ttl: Sekunden, die ein Cache-Eintrag als 'frisch' gilt (Default 300) + """ + return self._items_cache + + # ==== Bulk-Fetch: /emby/Items?Ids=...&Fields=... ==== + # ==== Bulk-Fetch: /emby/Items?Ids=...&Fields=... ==== + def get_items_bulk(self, ids: list[str], fields: list[str] | None = None) -> dict[str, dict]: + """ + Holt Emby-Items in Batches (Ids=...), gibt dict[id] -> item zurück. + Nutzt einen lokalen Cache, lädt nur fehlende/abgelaufene/unvollständige Einträge nach. + """ + import time # lokal, um keine Modulweite Änderung zu erzwingen + + self._ensure_http_session() + + if not ids: + return {} + + # Nur numerische/echte IDs abfragen (Platzhalter-Namen überspringen) + fetch_ids_all = [str(i) for i in ids if str(i).isdigit()] + if not fetch_ids_all: + return {} + + # Felder normalisieren (Mengenvergleich), Request-Parameter aber in stabiler Reihenfolge senden + fields = fields or [] + req_fields_set = {f for f in fields if f} + fields_param = ",".join(sorted(req_fields_set)) if req_fields_set else "" + + CHUNK = 200 # konservativ; Emby kommt damit gut klar + out: dict[str, dict] = {} + + now = time.time() + ttl = self.items_cache_ttl + + def is_fresh(ts: float) -> bool: + if not ttl: + return True # ttl == 0 => nie ablaufen lassen + return (now - ts) <= ttl + + # 1) Aufteilen in bekannte (frisch + Feld-Superset) und nachzuladende IDs + to_fetch: list[str] = [] + for id_ in fetch_ids_all: + cached_item = self._items_cache.get(id_) + cached_fields = self._items_cache_fields.get(id_, set()) + ts = self._items_cache_ts.get(id_, 0.0) + + if cached_item and is_fresh(ts) and req_fields_set.issubset(cached_fields): + out[id_] = cached_item + else: + to_fetch.append(id_) + + # 2) Fehlende/nicht vollständige Items in Batches nachladen + if to_fetch: + url = f"{self.emby_server_url}/emby/Items" + for i in range(0, len(to_fetch), CHUNK): + batch = to_fetch[i:i + CHUNK] + params = {"Ids": ",".join(batch), "api_key": self.api_key} + if fields_param: + params["Fields"] = fields_param + + resp = self.session.get(url, headers=self.headers, params=params, timeout=120) + resp.raise_for_status() + data = resp.json() or {} + items = data.get("Items") or data.get("items") or [] + + now_fetch = time.time() + for it in items: + it_id = str(it.get("Id") or "") + if not it_id: + continue + + # Bestehendes Cache-Objekt erweitern/überschreiben (merge) + if it_id in self._items_cache: + self._items_cache[it_id].update(it) + # wir wissen, dass mindestens die angeforderten Fields zurückkamen: + self._items_cache_fields[it_id] |= req_fields_set + else: + self._items_cache[it_id] = it + self._items_cache_fields[it_id] = set(req_fields_set) + + self._items_cache_ts[it_id] = now_fetch + out[it_id] = self._items_cache[it_id] + + return out + + # ==== Provider-IDs normalisieren ==== + def _norm_provider_ids(self, provider_ids: dict | None): + """Gibt (lowercased_dict, tmdb_id_str|None) zurück.""" + d = {(k or "").lower(): v for k, v in (provider_ids or {}).items()} + # Manche Emby-Instanzen speichern als int; alles in str + tmdb_val = d.get("tmdb") or d.get("tmdbid") or d.get("themoviedb") or None + tmdb_str = str(tmdb_val) if tmdb_val not in (None, "", 0) else None + return d, tmdb_str + + + def sync_people(self, emby_item: dict, my_cast: list, my_crew: list): + # ---------- Init ---------- + changed = False + log = { + "tmdb_added": [], + "aliases_prepared": [], + "aliases_reverted": [], + "demoted": [], + "created": [], + "created_pending": 0, + "namefix": [], # keine Sofort-Namefixes + "item_updated": False, + } + + demoted_pids_local = set() + + # interne Caches (einmalig pro Prozesslauf) + + def _cheap_key(lst, with_id=False): + # Multiset über (Type, Role, BaseName) – optional mit Id + key = {} + for p in (lst or []): + typ = p.get("Type") or "" + role = p.get("Role") or None + name = (p.get("Name") or "").strip() + base = _re.sub(r"(?:-\d+)+$", "", name).strip() # Alias-Suffixe wie "-12345" weg + k = (typ, role, base) if not with_id else (typ, role, base, str(p.get("Id") or "")) + key[k] = key.get(k, 0) + 1 + return key + + current_people = emby_item.get("People", []) or [] + desired_people = self.build_emby_people_from_tmdb(my_cast, my_crew, provider="Tmdb") + # return False, [], [] + # 1) Alias/ID-unabhängige Struktur (Type, Role, BaseName) + names_eq = (_cheap_key(current_people, with_id=False) == + _cheap_key(desired_people, with_id=False)) + + # 2) Streng inkl. Id (falls du unterscheiden willst, ob nur Id/Name abweichen) + full_eq = (_cheap_key(current_people, with_id=True) == + _cheap_key(desired_people, with_id=True)) + + if names_eq and full_eq: + # wirklich gar nichts zu tun + return False, "", [] + + # return False, "", [] + + # ... dein _cheap_key / names_eq / full_eq oben ... + + # if names_eq and not full_eq: + # # Schneller „REPLACE-only“-Pfad + # people_payload = [] + # people_alias_revert = [] # (pid, (alias_name, clean_name, tid)) + # _seen_ar = set() + # + # for dp in (desired_people or []): + # typ = dp.get("Type") or "" + # role = dp.get("Role") + # pid_raw = str(dp.get("Id") or "") + # name_raw = (dp.get("Name") or "").strip() + # tid = dp.get("Tmdb") or dp.get("tmdb") + # + # # Alias-Basename (entfernt evtl. vorhandene -\d Suffixe) + # base = _re.sub(r"(?:-\d+)+$", "", name_raw).strip() + # + # if (not pid_raw.isdigit()) and tid: + # # NEU: Nicht-numerische Id -> Alias in Name UND Id schreiben + # alias = f"{base}-{int(tid)}" + # q = {"Id": alias, "Name": alias, "Type": typ} + # if role is not None: + # q["Role"] = role + # people_payload.append(q) + # # (Noch keine people_alias_revert, da keine numerische PID existiert) + # continue + # + # # Standardfall (numerische PID oder kein TMDb vorhanden) + # q = {"Id": pid_raw, "Name": name_raw, "Type": typ} + # if role is not None: + # q["Role"] = role + # people_payload.append(q) + # + # # Downstream-Rename einsammeln, falls numerische PID + Alias im Namen + # if "-" in name_raw: + # base2, suf = name_raw.rsplit("-", 1) + # if base2 and suf.isdigit() and pid_raw.isdigit(): + # key = (pid_raw, name_raw) + # if key not in _seen_ar: + # _seen_ar.add(key) + # people_alias_revert.append((pid_raw, (name_raw, base2.strip(), suf))) + # + # # Hard-Replace nur wenn PIDs entfernt wurden + # prev_ids = {str(p.get("Id") or "") for p in (current_people or [])} + # next_ids = {str(p.get("Id") or "") for p in (people_payload or [])} + # removed_ids_present = bool(prev_ids - next_ids) + # + # if removed_ids_present and current_people: + # try: + # self.update_item(emby_item["Id"], {"People": []}) + # except Exception: + # pass + # + # # Zielzustand schreiben + # self.update_item(emby_item["Id"], {"People": people_payload}) + # + # # Kompakte Edits sind optional – wenn du nichts loggen willst, gib leer zurück + # return False, None, people_alias_revert + + # else: Struktur wirklich anders -> normaler (langsamer) Pfad + + # ---- EARLY PATH 2: gleiche Struktur (Type,Role,Basename), aber Id/Name differieren ---- + + + + people_alias_revert = [] # (pid, (alias_name/old_name, clean_name/new_name, tmdb_id)) + _seen_alias_reverts = set() # (pid, alias_or_old_name) + + # ---- Helpers (keine neuen Imports) ---- + def _alias_parts(name: str): + s = (name or "").strip() + if not s: + return None, None + s = s.replace("–", "-").replace("—", "-") # Unicode-Hyphen + m = _re.match(r"^(?P<clean>.+?)(?:-(?P<tid>\d+))+$", s) + if not m: + return None, None + clean = (m.group("clean") or "").strip() + tid = m.group("tid") + return clean, tid + + def _base_name(nm: str) -> str: + return _re.sub(r"(?:-\d+)+$", "", (nm or "")).strip() + + def _strip_alias_suffix(name, tmdb_id): + try: + suf = f"-{int(tmdb_id)}" + except Exception: + return (name or "").strip() + s = (name or "").strip() + if not s.endswith(suf): + return s + while s.endswith(suf): + s = s[: -len(suf)] + return s.strip() + + def _alias_once(name, tmdb_id): + base = _strip_alias_suffix(name, tmdb_id) + try: + return f"{base}-{int(tmdb_id)}" + except Exception: + return base + + def _pid_tmdb_from_bulk(bulk_map, pid: str): + it = bulk_map.get(str(pid)) or {} + prov, e_tmdb = self._norm_provider_ids(it.get("ProviderIds")) + return e_tmdb + + def _tmdb_from_person_hit(hit): + prov, e_tmdb = self._norm_provider_ids((hit or {}).get("ProviderIds")) + return e_tmdb + + def _hard_replace_people(item_id: str, people: list): + """setzt People exakt (kein Merge)""" + # try: + # self.update_item(item_id, {"People": []}) + # except TypeError: + # self.update_item(item_id, {"People": []}) + self.update_item(item_id, {"People": people}) + + # ---------- Crash-Recovery: vorhandene Aliase vormerken (sehr billig) ---------- + for p in (current_people or []): + nm = (p.get("Name") or "").strip() + clean, tid = _alias_parts(nm) + if not (clean and tid): + continue + pid = str(p.get("Id") or "") + if not pid: + continue + key = (pid, nm) + if key in _seen_alias_reverts: + continue + people_alias_revert.append((pid, (nm, clean, tid))) + _seen_alias_reverts.add(key) + + # ---------- Super-schneller Fast-Path (No-Op) ---------- + # Wenn Struktur & Basenamen 1:1 gleich sind, können wir vorzeitig raus. + def _cheap_key(lst): + # (Type, Id, Role, BaseName(Name)) -> Set + out = set() + for p in (lst or []): + out.add(( + p.get("Type") or "", + str(p.get("Id") or ""), + p.get("Role") or None, + _base_name(p.get("Name")), + )) + return out + + if _cheap_key(current_people) == _cheap_key(desired_people): + # Nichts zu tun – auch keine teuren lookups nötig + return False, [], people_alias_revert + + # ---------- PHASE A: Duplikate (gleiche TMDb) vorbereiten (per Cache) ---------- + numeric_pids = set( + [str(p.get("Id")) for p in current_people if str(p.get("Id") or "").isdigit()] + + [str(p.get("Id")) for p in desired_people if str(p.get("Id") or "").isdigit()] + ) + missing = [pid for pid in numeric_pids if pid not in self._bulk_person_cache] + if missing: + fetched = self.get_items_bulk(sorted(missing), fields=["ProviderIds", "Name"]) or {} + # cache auffüllen + for k, v in (fetched or {}).items(): + self._bulk_person_cache[str(k)] = v + # lokales Bild zusammensetzen + id_to_item = {pid: self._bulk_person_cache.get(pid, {}) for pid in numeric_pids} + + desired_pid_by_tmdb = {} + for dp in (desired_people or []): + tid = dp.get("Tmdb") or dp.get("tmdb") + pid = str(dp.get("Id") or "") + if tid and str(pid).isdigit() and (tid not in desired_pid_by_tmdb): + try: + desired_pid_by_tmdb[int(tid)] = pid + except Exception: + pass + + tmdb_groups = {} + for pid in numeric_pids: + tmdb_id = _pid_tmdb_from_bulk(id_to_item, pid) + if not tmdb_id: + continue + k = str(tmdb_id) + tmdb_groups.setdefault(k, set()).add(str(pid)) + + # Demote führen wir SPÄTER aus (nach dem Film-Write) und nie für PIDs, die verwendet werden. + + # ---------- PHASE B: Vergleichs-Signaturen (ohne Namen) ---------- + current_people_norm = list(current_people) + cur_sig = self._people_signature_no_name(current_people_norm) + des_sig = self._people_signature_no_name(desired_people) + + # Index aktuelle Personen nach (Type,Role,BaseName) + cur_idx = { + (p.get("Type") or "", p.get("Role") or None, _base_name(p.get("Name"))): p + for p in current_people_norm + } + + # ---------- PHASE C: TMDb an bestehender Person ergänzen (idempotent) ---------- + for dp in desired_people: + key = (dp.get("Type") or "", dp.get("Role") or None, _base_name(dp.get("Name"))) + cp = cur_idx.get(key) + if not cp: + continue + dp_tmdb = dp.get("Tmdb") or dp.get("tmdb") + if not dp_tmdb: + continue + cp_id = str(cp.get("Id") or "") + dp_id = str(dp.get("Id") or "") + if cp_id != dp_id or not cp_id.isdigit(): + continue + e_tmdb = _pid_tmdb_from_bulk(id_to_item, cp_id) + if not e_tmdb: + # nur schreiben, wenn wirklich fehlt + self.update_item(cp_id, {"Id": cp_id, "ProviderIds": {"Tmdb": str(dp_tmdb)}}) + log["tmdb_added"].append((cp_id, key[2], dp_tmdb)) + changed = True + + # ---------- PHASE D: temporäre Aliase nur wenn nötig ---------- + # Name->PID Index (Alias + Basename mappen) + cur_by_name = {} + for p in (current_people or []): + nm = (p.get("Name") or "").strip() + pid = str(p.get("Id") or "") + if not nm: + continue + cur_by_name[nm] = pid + base = _base_name(nm) + if base and base != nm and base not in cur_by_name: + cur_by_name[base] = pid + + # Persistente False-Friend-Namen laden + false_friend_names = self.config.Cache.query_false_friend_names() + + def _is_false_friend(clean_name: str, wanted_pid: str, wanted_tid): + cn = (clean_name or "").strip() + if not cn: + return False + key = cn.casefold() + + # 1) Explizit markierter Name? + if key in false_friend_names: + return True + + # 2) Auf dem aktuellen Item existiert derselbe Name, aber andere PID + cur_pid_x = cur_by_name.get(cn) + if cur_pid_x and cur_pid_x != wanted_pid: + return True + + # 3) Globaler Namenskonflikt: gleicher Name, aber anderer TMDb + for it in _global_hits_for(cn): + if (it.get("Name") or "").strip() != cn: + continue + hit_tid = _tmdb_from_person_hit(it) + if hit_tid is None or wanted_tid is None: + continue + if str(hit_tid) != str(wanted_tid): + return True + return False + + # Name -> Menge unterschiedlicher Ziel-IDs + name_to_target_ids = {} + for dp in (desired_people or []): + tid = dp.get("Tmdb") or dp.get("tmdb") + clean = _strip_alias_suffix(dp.get("Name") or "", tid) + pid = str(dp.get("Id") or "") + target = pid if pid.isdigit() else (f"{clean}-{tid}" if tid else clean) + s = name_to_target_ids.get(clean) + if s is None: + s = set() + name_to_target_ids[clean] = s + s.add(target) + need_alias_names = {nm for nm, ids in name_to_target_ids.items() if len(ids) > 1} + + # Kandidaten für globale Dubletten nur einmal pro Name suchen (Cache) + def _global_hits_for(clean_name: str): + if clean_name in self._person_name_cache: + return self._person_name_cache[clean_name] + try: + hits = self.get_person_by_name(clean_name) or [] + except Exception: + hits = [] + self._person_name_cache[clean_name] = hits + return hits + + temp_renames = {} # emby_pid -> (alias_name, clean_name, tmdb) + people_payload = [] # gewünschte Liste (ohne Tmdb-Key) + + for dp in (desired_people or []): + pid = str(dp.get("Id") or "") + typ = dp.get("Type") or "" + role = dp.get("Role") + tid = dp.get("Tmdb") or dp.get("tmdb") + clean = _strip_alias_suffix(dp.get("Name") or "", tid) + + if not pid.isdigit(): + if not tid: + continue + alias = _alias_once(clean, tid) + q = {"Id": alias, "Name": alias, "Type": typ} + if role: + q["Role"] = role + people_payload.append(q) + continue + + need_alias = False + if clean in need_alias_names: + need_alias = True + if not need_alias: + # einmal lokal pro Lauf merken, damit derselbe pid nicht doppelt demotet wird + # (am Anfang von sync_people definieren, direkt nach den Log-Variablen): + # demoted_pids_local = set() + + cur_pid = cur_by_name.get(clean) + if cur_pid and cur_pid != pid: + need_alias = True + # Nur demoten, wenn: + # - Ziel-PID NUMERISCH ist (echte Emby-Person) + # - und der aktuelle Datensatz dieselbe TMDb-ID hat (echtes Duplikat) + # ODER keine TMDb-ID hinterlegt ist (offensichtlich „falsch/leer“) + if str(pid).isdigit(): + cur_tmdb = _pid_tmdb_from_bulk(id_to_item, cur_pid) + if (tid is not None and str(cur_tmdb) == str(tid)) or (cur_tmdb is None): + if cur_pid not in demoted_pids_local: + self._demote_duplicate_person(cur_pid) + demoted_pids_local.add(cur_pid) + try: + logger.info( + f"[sync_people] Demoted duplicate person pid={cur_pid} (name='{clean}', tmdb={cur_tmdb})") + except Exception: + pass + else: + # False Friend (gleicher Name, andere TMDb): NICHT demoten + try: + self.config.Cache.add_false_friend_name(clean) + except Exception: + pass + # Wenn pid NICHT numerisch (Alias), grundsätzlich KEIN Demote. + + # False-Friend anhand DB/Lokalkontext/Globalcheck ermitteln + need_alias_ff = _is_false_friend(clean, pid, tid) + + # Dein existierendes need_alias bleibt unverändert nutzbar + if need_alias or need_alias_ff: + # bei neu erkanntem False-Friend sofort persistieren (idempotent) + if need_alias_ff and clean.casefold() not in false_friend_names: + self.config.Cache.add_false_friend_name(clean) + + alias = _alias_once(clean, tid) if tid else clean + temp_renames[pid] = (alias, clean, tid) + q = {"Id": pid, "Name": alias, "Type": typ} + else: + q = {"Id": pid, "Name": clean, "Type": typ} + if role: + q["Role"] = role + people_payload.append(q) + + # ----- Update-Entscheidung (idempotent) ----- + def _namekey(lst): + def _norm_role(r): return "" if r is None else str(r) + + out = [] + for p in (lst or []): + out.append(( + str(p.get("Type") or ""), + str(p.get("Id") or ""), + _norm_role(p.get("Role")), + (p.get("Name") or "").strip(), + )) + out.sort() + return out + + current_namekey = _namekey(current_people) + desired_namekey = _namekey(people_payload) + + need_struct_update = (cur_sig != des_sig) or any(not str(p.get("Id") or "").isdigit() for p in people_payload) + need_name_update = (current_namekey != desired_namekey) + need_item_update = need_struct_update or need_name_update + + # 3) Demote jetzt, aber nie PIDs demoten, die wir eben verwenden + used_pids_now = {str(p.get("Id") or "") for p in (people_payload or [])} + for tmdb_id, pids in tmdb_groups.items(): + if len(pids) <= 1: + continue + # Kanonische PID wählen + canonical = None + try: + t_int = int(tmdb_id) + except Exception: + t_int = None + if t_int is not None: + cand = desired_pid_by_tmdb.get(t_int) + if cand and cand in pids: + canonical = cand + if not canonical: + canonical = sorted(pids, key=lambda s: int(s))[0] + for pid in pids: + if pid == canonical or pid in used_pids_now: + continue + # idempotent: nur setzen, wenn Name nicht schon "John Doe" oder ProviderIds nicht leer + cur_it = self._bulk_person_cache.get(pid) or {} + cur_nm = (cur_it.get("Name") or "").strip() + cur_pids = (cur_it.get("ProviderIds") or {}) or {} + if cur_nm == "John Doe" and not cur_pids: + continue + self._demote_duplicate_person(pid) + self._bulk_person_cache[pid] = {"Name": "John Doe", "ProviderIds": {}} + log["demoted"].append((tmdb_id, pid)) + changed = True + + if need_item_update: + # 1) Aliase setzen (nur wenn nötig; idempotent) + for emby_pid, (alias_name, clean_name, tid) in temp_renames.items(): + # idempotent: nur schreiben, wenn Name != alias_name + cur_info = self._bulk_person_cache.get(str(emby_pid)) or {} + cur_name_now = (cur_info.get("Name") or "").strip() + if cur_name_now != alias_name: + if not self.update_item(emby_pid, {"Id": emby_pid, "Name": alias_name, "ProviderIds": {"Tmdb": tid}}): + continue + # Cache aktualisieren, um Folge-Calls zu sparen + cur_info = dict(cur_info) + cur_info["Name"] = alias_name + cur_info["ProviderIds"] = {"Tmdb": tid} + self._bulk_person_cache[str(emby_pid)] = cur_info + log["aliases_prepared"].append((emby_pid, alias_name)) + changed = True + + key = (str(emby_pid), alias_name) + if key not in _seen_alias_reverts: + people_alias_revert.append( + (str(emby_pid), (alias_name, clean_name, str(tid) if tid is not None else None))) + _seen_alias_reverts.add(key) + + # 2) Film-Update EXAKT (kein Merge), aber nur wenn wirklich unterschiedlich + _hard_replace_people(emby_item["Id"], people_payload) + log["item_updated"] = True + changed = True + + + # ---------- PHASE E: Name-Fixes (nur sammeln; keine Sofort-Updates) ---------- + def _is_alias_name(nm: str) -> bool: + s = (nm or "").strip() + if "-" in s: + suf = s.rsplit("-", 1)[-1] + return suf.isdigit() + return False + + has_aliases = any(_is_alias_name(p.get("Name")) for p in (people_payload or [])) + cur_sig2 = self._people_signature_no_name(people_payload) + des_sig2 = self._people_signature_no_name(desired_people) + + if (cur_sig2 == des_sig2) and not has_aliases: + tmdb_by_pid = { + str(p.get("Id")): p.get("Tmdb") + for p in desired_people + if str(p.get("Id") or "").isdigit() and p.get("Tmdb") + } + desired_for_namefix = [] + for dp in desired_people: + nm = dp.get("Name") or "" + tid = dp.get("Tmdb") + if tid and str(nm).endswith(f"-{tid}"): + dp2 = dict(dp) + dp2["Name"] = nm[:-(len(str(tid)) + 1)] + desired_for_namefix.append(dp2) + else: + desired_for_namefix.append(dp) + + fixes = self._collect_person_name_fixes(current_people_norm, desired_for_namefix) or [] + for pid, cur_name, new_name in fixes: + tid = tmdb_by_pid.get(str(pid)) + tid_str = str(tid) if tid is not None else None + clean_cur, tid_cur = _alias_parts(cur_name) + if clean_cur: + key = (str(pid), cur_name) + if key not in _seen_alias_reverts: + people_alias_revert.append((str(pid), (cur_name, clean_cur, tid_cur or tid_str))) + _seen_alias_reverts.add(key) + continue + key = (str(pid), cur_name) + if key not in _seen_alias_reverts: + people_alias_revert.append((str(pid), (cur_name, new_name, tid_str))) + _seen_alias_reverts.add(key) + # kein sofortiges Update + + # ---------- Defensiver Fallback: finalen Payload scannen ---------- + try: + for p in (people_payload or []): + pid = str(p.get("Id") or "") + nm = (p.get("Name") or "").strip() + clean, tid = _alias_parts(nm) + if pid.isdigit() and clean and tid: + key = (pid, nm) + if key not in _seen_alias_reverts: + people_alias_revert.append((pid, (nm, clean, tid))) + _seen_alias_reverts.add(key) + except Exception: + pass + + # ---------- Zusammenfassung ---------- + def build_people_change_events(current_people, desired_people, log=None): + log = log or {} + events = [] + + def _nm(p): + return (p.get("Name") or "").strip() + + def _role(p): + return p.get("Role") or None + + def _type(p): + return (p.get("Type") or "") + + def _pkey(p): + return (_type(p), str(p.get("Id") or ""), _role(p)) + + def _is_uncredited(p): + r = (_role(p) or "").lower() + n = _nm(p).lower() + return "uncredited" in r or "uncredited" in n + + def _fmt_people_list(persons, limit=6, with_pid=True, with_role=True): + out = [] + for p in persons[:limit]: + parts = [_nm(p)] + meta = [] + if with_role and _role(p): + meta.append(f"role={_role(p)}") + if with_pid: + meta.append(f"pid={p.get('Id')}") + if meta: + parts.append(f"({', '.join(meta)})") + out.append(" ".join(parts)) + if len(persons) > limit: + out.append(f"... +{len(persons) - limit} weitere") + return "; ".join(out) + + def _tr_key(tr): + typ, role = tr + return (str(typ or ""), "" if role is None else str(role)) + + prev_map = {_pkey(p): p for p in (current_people or [])} + next_source = desired_people or [] # = people_payload + next_map = {_pkey(p): p for p in next_source} + + added_keys = list(next_map.keys() - prev_map.keys()) + removed_keys = list(prev_map.keys() - next_map.keys()) + added_people = [next_map[k] for k in added_keys] + removed_people = [prev_map[k] for k in removed_keys] + + from collections import defaultdict + add_by_tr = defaultdict(list) + rem_by_tr = defaultdict(list) + for p in added_people: + add_by_tr[(_type(p), _role(p))].append(p) + for p in removed_people: + rem_by_tr[(_type(p), _role(p))].append(p) + + replaced_pairs = [] + for tr in sorted(set(add_by_tr.keys()) | set(rem_by_tr.keys()), key=_tr_key): + adds = add_by_tr.get(tr, []) + rems = rem_by_tr.get(tr, []) + while adds and rems: + a = adds.pop(0) + r = rems.pop(0) + replaced_pairs.append((r, a)) + add_by_tr[tr] = adds + rem_by_tr[tr] = rems + + def _group_by_type(lst): + g = defaultdict(list) + for p in (lst or []): + g[_type(p)].append(p) + return g + + grouped_adds = _group_by_type([p for lst in add_by_tr.values() for p in lst]) + grouped_rems = _group_by_type([p for lst in rem_by_tr.values() for p in lst]) + + for typ in sorted(grouped_adds.keys(), key=lambda s: s or ""): + persons = grouped_adds[typ] + events.append(f"ADD {typ}: " + _fmt_people_list(persons, limit=6, with_pid=True, with_role=True)) + + for typ in sorted(grouped_rems.keys(), key=lambda s: s or ""): + persons = grouped_rems[typ] + credited = [p for p in persons if not _is_uncredited(p)] + uncred = [p for p in persons if _is_uncredited(p)] + if credited: + events.append( + f"REMOVE {typ}: " + _fmt_people_list(credited, limit=6, with_pid=True, with_role=True)) + if uncred: + events.append(f"REMOVE {typ} (uncredited): {len(uncred)} entfernt") + + for r, a in replaced_pairs: + role_txt = f" role={_role(a)}" if _role(a) else "" + events.append( + f"REPLACE {_type(a)}{role_txt}: {_nm(r)} (pid={r.get('Id')}) -> {_nm(a)} (pid={a.get('Id')})" + + (f", tmdb={a.get('Tmdb')}" if a.get('Tmdb') else "") + ) + + tmdb_added = log.get("tmdb_added") or [] + if tmdb_added: + preview = "; ".join([f"{name} (pid={pid}, tmdb={tm})" for pid, name, tm in tmdb_added[:6]]) + more = f"; ... +{len(tmdb_added) - 6} weitere" if len(tmdb_added) > 6 else "" + events.append("TMDB_ADD: " + preview + more) + + aliases_prep = log.get("aliases_prepared") or [] + if aliases_prep: + preview = "; ".join([f"{alias} (pid={pid})" for pid, alias in aliases_prep[:6]]) + more = f"; ... +{len(aliases_prep) - 6} weitere" if len(aliases_prep) > 6 else "" + events.append("ALIAS_PREPARE: " + preview + more) + + dem = log.get("demoted") or [] + if dem: + preview = "; ".join([f"tmdb={t} pid={p}" for t, p in dem[:8]]) + more = f"; ... +{len(dem) - 8} weitere" if len(dem) > 8 else "" + events.append("DEMOTED: " + preview + more) + + if log.get("created_pending"): + events.append(f"CREATED_PENDING x{int(log['created_pending'])}") + + return events + + # ---------- Return ---------- + # dedupe & sort für saubere Downstream-Liste + if people_alias_revert: + dedup = [] + seen = set() + for pid, (alias, clean, tid) in people_alias_revert: + key = (str(pid), str(alias)) + if key in seen: + continue + seen.add(key) + dedup.append((str(pid), (alias, clean, tid))) + people_alias_revert = sorted(dedup, key=lambda t: (t[1][0].casefold(), t[0])) + + if not changed: + return False, "", people_alias_revert + + item_edits = build_people_change_events( + current_people=current_people, + desired_people=people_payload, # gegen tatsächlichen Schreibzustand diffen + log=log + ) + return True, "; ".join(item_edits), people_alias_revert + + # --- Zeichentests / Normalisierung --- + def _is_latin_string(self, s: str) -> bool: + if not s: + return False + # akzeptiere Basis- und erweiterte Latin-Blocks + übliche Satzzeichen/Leerzeichen + return all( + (('LATIN' in unicodedata.name(ch, '')) or not ch.isalpha()) + for ch in s + ) + + def _ascii_sort(self, s: str) -> str: + return self._strip_accents(s or '').strip() + + # --- TMDb: helper, robust gegen fehlende Felder --- + def _tmdb_person_translated_name_en(self, person_id: int | str) -> str | None: + """ + Holt /person/{id}/translations und gibt data.name für 'en' oder 'en-US' zurück, falls vorhanden. + """ + try: + # tmdbapis RAW-V3: person_get_translations + my_tmdb= self.config.TMDb + data = my_tmdb.API3.person_get_translations(int(person_id)) + trs = (data or {}).get("translations") or [] + # erst en-US, dann en + for pref in ("en-US", "en"): + for t in trs: + if (t.get("iso_639_1") == "en" and (t.get("iso_3166_1") in (None, "", "US"))) or \ + (f'{t.get("iso_639_1", "")}-{t.get("iso_3166_1", "")}' == pref): + name = ((t.get("data") or {}).get("name") or "").strip() + if name: + return name + # generisch: irgendeine englische Übersetzung + for t in trs: + if t.get("iso_639_1") == "en": + name = ((t.get("data") or {}).get("name") or "").strip() + if name: + return name + except Exception: + pass + return None + + def _tmdb_person_alias_latin(self, person_id: int | str) -> str | None: + """ + Holt /person/{id} (Details) und wählt einen latinischen Alias aus also_known_as. + """ + try: + # RAW-V3: person_get_details; language hier egal, Name/Aliase sind nicht lokalisiert + my_tmdb= self.config.TMDb + p = my_tmdb.API3.person_get_details(int(person_id)) + aliases = (p or {}).get("also_known_as") or [] + for alias in aliases: + a = (alias or "").strip() + if a and self._is_latin_string(self, a): + return a + except Exception: + pass + return None + + def _romanize_local(self, name: str) -> str: + """ + Letzter Fallback: lokale Transliteration (optional). + """ + try: + from unidecode import unidecode + r = unidecode(name or "").strip() + return r if r else name + except Exception: + # notfalls Akzente nur strippen + return self._strip_accents(name or "").strip() + + + def get_romanized_person_name(self, tmdb_person_id: int | str, original_name: str) -> str | None: + """ + Liefert eine latinische Schreibweise für die Person oder None, wenn nichts Brauchbares gefunden wurde. + Reihenfolge: translations(en) -> alias -> local translit. + """ + key = str(tmdb_person_id) + if key in self._roman_name_cache: + return self._roman_name_cache[key] + + if self._is_latin_string(original_name or ""): + self._roman_name_cache[key] = original_name + return original_name + + # 1) Übersetzung (en) + name = None + # name = self._tmdb_person_translated_name_en(tmdb_person_id) + if not name: + pass + # 2) Alias + # name = self._tmdb_person_alias_latin(tmdb_person_id) + if not name: + # 3) Lokale Transliteration (optional) + name = self._romanize_local(original_name or "") + + # Validieren: wirklich latinisch? + if name and self._is_latin_string(name): + self._roman_name_cache[key] = name + return name + + self._roman_name_cache[key] = None + return None + + # --- Anwenden beim Personen-Fix (zentraler Emby-Person-Datensatz) --- + def ensure_person_latin_name(self, emby_person_id: str, tmdb_person_id: int | str, current_name: str): + """ + Sorgt dafür, dass der zentrale Emby-Personendatensatz einen latinischen Namen hat. + Setzt zusätzlich ForcedSortName (akzentfrei), damit die Sortierung stimmt. + """ + return current_name + + # if tmdb_person_id == 21909 or current_name == "鄭伊健": + # # Identitäten zeigen (rein zu Debug-Zwecken) + # print("old_name:", repr(old_name)) + # print("current_name:", repr(current_name)) + # pass + latin = self.get_romanized_person_name(tmdb_person_id, current_name) + + # if tmdb_person_id == 21909 or current_name == "鄭伊健": + # print(f"Item {tmdb_person_id} - {current_name} - {latin}") + + if not latin or latin == current_name: + # print(f"Is None or not equal: {tmdb_person_id} - {current_name} - {latin}") + return None # nichts zu tun + + # --- Debug VOR ascii_sort, damit du etwas siehst, falls ascii_sort crasht --- + # print(f"About to rename: tmdb={tmdb_person_id} emby={emby_person_id} '{current_name}' -> '{latin}'") + + # --- Hart absichern: ascii_sort kann crashen --- + try: + forced = self._ascii_sort(latin) + except Exception as e: + print(f"[ascii_sort ERROR] tmdb={tmdb_person_id} latin={latin!r} err={e!r}") + forced = latin # Fallback: notfalls identisch vergeben + + payload = { + "Name": latin, + "ForcedSortName": forced, + } + changed_name = f"{current_name}' → '{latin}" + print(f"Changed person name '{current_name}' to '{latin}'") + return self.update_item(emby_person_id, payload) + + + def _bind_placeholders_to_existing(self, current_people: list[dict], desired_people: list[dict]) -> bool: + """ + Sucht in current_people (ohne Alias) passende Personen per Name+Type und + ersetzt Platzhalter in desired_people durch die vorhandene Emby-ID. + Hängt der gefundenen Person sofort die TMDb-ProviderId an. + """ + # Index: (etype, norm_name) -> emby_id + by_name = {} + for p in (current_people or []): + et = p.get("Type") or "" + nm = self._norm_name(p.get("Name") or "") + if et and nm: + by_name[(et, nm)] = str(p.get("Id") or "") + + changed = False + for dp in (desired_people or []): + et = dp.get("Type") or "" + tmdb = dp.get("Tmdb") + nm_clean = self._strip_alias_suffix(dp.get("Name") or "", tmdb) + # nur Platzhalter (Id nicht numerisch) binden + if str(dp.get("Id") or "").isdigit(): + continue + emby_existing = by_name.get((et, self._norm_name(nm_clean))) + emby_existing_dupe = by_name.get((et, self._norm_name(f"{dp.get("Name")}-{tmdb}"))) + if emby_existing_dupe: + pass + if emby_existing or emby_existing_dupe: + dp["Id"] = emby_existing or emby_existing_dupe + dp["Name"] = nm_clean # Alias weg + # provider sofort an die Person hängen + if tmdb: + self._attach_tmdb_to_person(emby_existing or emby_existing_dupe, int(tmdb)) + if self.config.Cache: + self.config.Cache.update_tmdb_person_map(False, int(tmdb), emby_id=emby_existing or emby_existing_dupe, + name=nm_clean, alias=None, + expiration=self.config.Cache.expiration) + changed = True + return changed + + + + diff --git a/modules/ergast.py b/modules/ergast.py index d6830eefd..f7dff8fbb 100644 --- a/modules/ergast.py +++ b/modules/ergast.py @@ -4,7 +4,7 @@ logger = util.logger -base_url = "http://ergast.com/api/f1/" +base_url = "https://api.jolpi.ca/ergast/f1/" translations = { "nl": { @@ -68,12 +68,22 @@ } class Race: - def __init__(self, data, language): + def __init__(self, data, language, round_prefix, shorten_gp): self._data = data self._language = language + self._round_prefix = round_prefix + self._shorten_gp = shorten_gp self.season = util.check_num(self._data["season"], is_int=True) self.round = util.check_num(self._data["round"], is_int=True) self.name = self._data["raceName"] + if self._language: + self.title = f"GP {self.name.replace(' Grand Prix', '')}" if shorten_gp else self.name + for eng_value, trans_value in translations[self._language].items(): + self.title = self.title.replace(eng_value, trans_value) + else: + self.title = self.name.replace("Grand Prix", "GP") if shorten_gp else self.name + if round_prefix: + self.title = f"{self.round:02} - {self.title}" try: self.date = datetime.strptime(self._data["date"], "%Y-%m-%d") except (ValueError, TypeError): @@ -82,16 +92,8 @@ def __init__(self, data, language): def __str__(self): return f"Season {self.season} Round {self.round}: {self.name}" - def format_name(self, round_prefix, shorten_gp): - if self._language: - output = f"GP {self.name.replace(' Grand Prix', '')}" if shorten_gp else self.name - for eng_value, trans_value in translations[self._language].items(): - output = output.replace(eng_value, trans_value) - else: - output = self.name.replace("Grand Prix", "GP") if shorten_gp else self.name - if round_prefix: - output = f"{self.round:02} - {output}" - return output + def __repr__(self): + return self.__str__() def session_info(self, title, sprint_weekend): title = title.lower() @@ -146,6 +148,7 @@ def session_info(self, title, sprint_weekend): output = "Ted's Race Notebook" else: output = "Race" + if "2160" in title or "4K" in title: output = f"{output} (4K)" @@ -168,15 +171,15 @@ def __init__(self, requests, cache): self.requests = requests self.cache = cache - def get_races(self, year, language, ignore_cache=False): + def get_races(self, year, language, round_prefix, shorten_gp, ignore_cache=False): expired = None if self.cache and not ignore_cache: race_list, expired = self.cache.query_ergast(year, self.cache.expiration) if race_list and expired is False: - return [Race(r, language) for r in race_list] + return [Race(r, language, round_prefix, shorten_gp) for r in race_list] response = self.requests.get(f"{base_url}{year}.json") if response.status_code < 400: - races = [Race(r, language) for r in response.json()["MRData"]["RaceTable"]["Races"]] + races = [Race(r, language, round_prefix, shorten_gp) for r in response.json()["MRData"]["RaceTable"]["Races"]] if self.cache and not ignore_cache: self.cache.update_ergast(expired, year, races, self.cache.expiration) return races diff --git a/modules/imdb.py b/modules/imdb.py index 171af835b..dc09a447d 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -374,18 +374,21 @@ def __init__(self, requests, cache, default_dir): self._ratings = None self._genres = None self._episode_ratings = None - self._events_validation = None - self._events = {} + self._git_events = {} + self._git_events_validation = None + self._web_events = {} + self._web_event_validation = {} self._search_hash = None self._list_hash = None self._watchlist_hash = None - self.event_url_validation = {} - def _request(self, url, language=None, xpath=None, params=None): + def _request(self, url, language=None, xpath=None, params=None, page_props=False): logger.trace(f"URL: {url}") if params: logger.trace(f"Params: {params}") response = self.requests.get_html(url, params=params, header=True, language=language) + if page_props: + return json.loads(response.xpath("//script[@id='__NEXT_DATA__']/text()")[0])["props"]["pageProps"] return response.xpath(xpath) if xpath else response def _graph_request(self, json_data): @@ -409,17 +412,6 @@ def watchlist_hash(self): self._watchlist_hash = self.requests.get(watchlist_hash_url).text.strip() return self._watchlist_hash - @property - def events_validation(self): - if self._events_validation is None: - self._events_validation = self.requests.get_yaml(f"{git_base}/event_validation.yml").data - return self._events_validation - - def get_event(self, event_id): - if event_id not in self._events: - self._events[event_id] = self.requests.get_yaml(f"{git_base}/events/{event_id}.yml").data - return self._events[event_id] - def validate_imdb(self, err_type, method, imdb_dicts): valid_lists = [] main = "list_id" if method == "imdb_list" else "user_id" @@ -476,33 +468,6 @@ def validate_imdb(self, err_type, method, imdb_dicts): valid_lists.append(new_dict) return valid_lists - def get_event_years(self, event_id): - if event_id in self.events_validation: - return True, self.events_validation[event_id]["years"] - if event_id not in self.event_url_validation: - self.event_url_validation[event_id] = [] - for event_link in self._request(f"{base_url}/event/{event_id}", xpath="//div[@class='event-history-widget']//a/@href"): - parts = event_link.split("/") - self.event_url_validation[event_id].append(f"{parts[3]}{f'-{parts[4]}' if parts[4] != '1' else ''}") - return False, self.event_url_validation[event_id] - - def get_award_names(self, event_id, event_year): - if event_id in self.events_validation: - return self.events_validation[event_id]["awards"], self.events_validation[event_id]["categories"] - award_names = [] - category_names = [] - event_slug = f"{event_year[0]}/1" if "-" not in event_year[0] else event_year[0].replace("-", "/") - for text in self._request(f"{base_url}/event/{event_id}/{event_slug}/?ref_=ev_eh", xpath="//div[@class='article']/script/text()")[0].split("\n"): - if text.strip().startswith("IMDbReactWidgets.NomineesWidget.push"): - jsonline = text.strip() - obj = json.loads(jsonline[jsonline.find("{"):-3]) - for award in obj["nomineesWidgetModel"]["eventEditionSummary"]["awards"]: - award_names.append(award["awardName"]) - for category in award["categories"]: - category_names.append(category["categoryName"]) - break - return award_names, category_names - def _json_operation(self, list_type): if list_type == "search": return "AdvancedTitleSearch", self.search_hash @@ -574,7 +539,7 @@ def check_constraint(bases, mods, constraint, lower="", translation=None, range_ input_list.extend([event_options[a] if a in event_options else {"eventId": a} for a in data["event"]]) if "event.winning" in data: for a in data["event.winning"]: - award_dict = event_options[a] if a in event_options else {"eventId": a} + award_dict = event_options[a].copy() if a in event_options else {"eventId": a} award_dict["winnerFilter"] = "WINNER_ONLY" input_list.append(award_dict) out["awardConstraint"] = {"allEventNominations": input_list} @@ -621,6 +586,7 @@ def check_constraint(bases, mods, constraint, lower="", translation=None, range_ out["lsConst"] = data["list_id"] else: out["urConst"] = data["user_id"] + out["isInPace"] = False out["sort"] = {"by": list_sort_by_options[sort_by], "order": sort_order.upper()} logger.trace(out) @@ -669,46 +635,6 @@ def _pagination(self, data, list_type): logger.error(f"Response: {response_json}") raise - def _award(self, data): - final_list = [] - if data["event_id"] in self.events_validation: - event_data = self.get_event(data["event_id"]) - if data["event_year"] == "all": - event_years = self.events_validation[data["event_id"]]["years"] - elif data["event_year"] == "latest": - event_years = [self.events_validation[data["event_id"]]["years"][0]] - else: - event_years = data["event_year"] - for event_year in event_years: - for award, categories in event_data[event_year].items(): - if data["award_filter"] and award not in data["award_filter"]: - continue - for cat in categories: - if data["category_filter"] and cat not in data["category_filter"]: - continue - final_list.extend(categories[cat]["winner" if data["winning"] else "nominee"]) - else: - event_year = self.get_event_years(data["event_id"])[0] if data["event_year"] == "latest" else data["event_year"][0] - event_slug = f"{event_year}/1" if "-" not in event_year else event_year.replace("-", "/") - for text in self._request(f"{base_url}/event/{data['event_id']}/{event_slug}/?ref_=ev_eh", xpath="//div[@class='article']/script/text()")[0].split("\n"): - if text.strip().startswith("IMDbReactWidgets.NomineesWidget.push"): - jsonline = text.strip() - obj = json.loads(jsonline[jsonline.find('{'):-3]) - for award in obj["nomineesWidgetModel"]["eventEditionSummary"]["awards"]: - if data["award_filter"] and award["awardName"] not in data["award_filter"]: - continue - for cat in award["categories"]: - if data["category_filter"] and cat["categoryName"] not in data["category_filter"]: - continue - for nom in cat["nominations"]: - if data["winning"] and not nom["isWinner"]: - continue - imdb_id = next((n["const"] for n in nom["primaryNominees"] + nom["secondaryNominees"] if n["const"].startswith("tt")), None) - if imdb_id: - final_list.append(imdb_id) - break - return final_list - def keywords(self, imdb_id, language, ignore_cache=False): imdb_keywords = {} expired = None @@ -880,3 +806,109 @@ def item_filter(self, imdb_info, filter_attr, modifier, filter_final, filter_dat or (list(set(filter_data["keywords"]) & set(attrs)) and modifier == ".not"): return False return True + + # Award Methods + + @property + def git_events_validation(self): + if self._git_events_validation is None: + self._git_events_validation = self.requests.get_yaml(f"{git_base}/event_validation.yml").data + return self._git_events_validation + + def git_event(self, event_id): + if event_id not in self._git_events: + self._git_events[event_id] = self.requests.get_yaml(f"{git_base}/events/{event_id}.yml").data + return self._git_events[event_id] + + def get_event_years(self, event_id): + if event_id in self.git_events_validation: + return True, self.git_events_validation[event_id]["years"] + if event_id not in self._web_event_validation: + self._web_event_validation[event_id] = [] + for year_data in self._request(f"{base_url}/event/{event_id}", page_props=True)["historyEventEditions"]: + extra = '' if year_data["instanceWithinYear"] == 1 else f"-{year_data['instanceWithinYear']}" + self._web_event_validation[event_id].append(f"{year_data['year']}{extra}") + return False, self._web_event_validation[event_id] + + def get_event_names(self, event_id, event_years): + award_names = [] + category_names = [] + for event_year in event_years: + event_data = self.get_event_data(event_id, event_year) + for award_name, categories in event_data.items(): + if award_name and award_name not in award_names: + award_names.append(award_name) + for category_name in categories: + if category_name and category_name not in category_names: + category_names.append(category_name) + return award_names, category_names + + def get_event_data(self, event_id, event_year): + if event_id in self.git_events_validation: + return self.git_event(event_id)[event_year] + if event_id not in self._web_events or event_year not in self._web_events[event_id]: + award_data = {} + event_slug = f"{event_year}/1" if "-" not in event_year else event_year.replace("-", "/") + for award in self._request(f"{base_url}/event/{event_id}/{event_slug}/?ref_=ev_eh", page_props=True)["edition"]["awards"]: + award_name = award["text"].lower() + if award_name not in award_data: + award_data[award_name] = {} + for cat in award["nominationCategories"]["edges"]: + cat_name = award_name if cat["node"]["category"] is None else cat["node"]["category"]["text"].lower() + nominees = [] + winners = [] + for nom in cat["node"]["nominations"]["edges"]: + if "awardTitles" in nom["node"]["awardedEntities"]: + prop = "awardTitles" + elif "secondaryAwardTitles" in nom["node"]["awardedEntities"] and nom["node"]["awardedEntities"]["secondaryAwardTitles"]: + prop = "secondaryAwardTitles" + else: + prop = None + if prop: + for award_title in nom["node"]["awardedEntities"][prop]: + imdb_id = award_title["title"]["id"] + if imdb_id: + nominees.append(imdb_id) + if nom["node"]["isWinner"]: + winners.append(imdb_id) + + if nominees or winners: + if cat_name not in award_data[award_name]: + award_data[award_name][cat_name] = {"nominee": [], "winner": []} + for n in nominees: + if n not in award_data[award_name][cat_name]["nominee"]: + award_data[award_name][cat_name]["nominee"].append(n) + for w in winners: + if w not in award_data[award_name][cat_name]["winner"]: + award_data[award_name][cat_name]["winner"].append(w) + if event_id not in self._web_events: + self._web_events[event_id] = {} + self._web_events[event_id][event_year] = award_data + return self._web_events[event_id][event_year] + + def _award(self, data): + final_list = [] + + if data["event_id"] in self.git_events_validation: + if data["event_year"] == "all": + event_years = self.git_events_validation[data["event_id"]]["years"] + elif data["event_year"] == "latest": + event_years = self.git_events_validation[data["event_id"]]["years"][:1] + else: + event_years = data["event_year"] + elif data["event_year"] == "latest": + event_years = self.get_event_years(data["event_id"])[:1] + else: + event_years = data["event_year"][:1] + + for event_year in event_years: + event_data = self.get_event_data(data["event_id"], event_year) + for award, categories in event_data.items(): + if data["award_filter"] and award not in data["award_filter"]: + continue + for cat in categories: + if data["category_filter"] and cat not in data["category_filter"]: + continue + final_list.extend(categories[cat]["winner" if data["winning"] else "nominee"]) + + return final_list diff --git a/modules/letterboxd.py b/modules/letterboxd.py index 60bb02c16..f5284b91b 100644 --- a/modules/letterboxd.py +++ b/modules/letterboxd.py @@ -25,23 +25,59 @@ def _request(self, url, language, xpath=None): return response.xpath(xpath) if xpath else response def _parse_page(self, list_url, language): - if "ajax" not in list_url: - list_url = list_url.replace("https://letterboxd.com/films", "https://letterboxd.com/films/ajax") + # Ajax nur für /films/-Browsen erzwingen; Listen-URLs so lassen + if "/films/" in list_url and "/ajax/" not in list_url: + list_url = list_url.replace("/films/", "/films/ajax/", 1) + response = self._request(list_url, language) - letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id") + + # Alle Film-Knoten einsammeln – funktioniert für Ajax, Grid & Detail + film_nodes = response.xpath("//div[@data-film-id]") items = [] - for letterboxd_id in letterboxd_ids: - slugs = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/@data-target-link") - comments = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/div/p/text()") - ratings = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']//span[contains(@class, 'rating')]/@class") - years = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/parent::li/div[@class='film-detail-content']/h2/small/a/text()") + + for node in film_nodes: + letterboxd_id = node.get("data-film-id") + + # Slug/Link: bevorzugt data-target-link, sonst data-item-link + slug = node.get("data-target-link") or node.get("data-item-link") or None + + # Versuche Detail-Infos aus der umgebenden <li> (falls vorhanden) + li = node.xpath("./ancestor::li[1]") + comment = None rating = None - if ratings: - match = re.search("rated-(\\d+)", ratings[0]) - if match: - rating = int(match.group(1)) - items.append((letterboxd_id, slugs[0], int(years[0]) if years else None, comments[0] if comments else None, rating)) - next_url = response.xpath("//a[@class='next']/@href") + year = None + + if li: + li = li[0] + # Kommentar (nur in Detail-Ansicht vorhanden) + comment = (li.xpath(".//div[@class='film-detail-content']/div/p/text()") or [None])[0] + + # Sterneklasse -> Zahl extrahieren (rated-8, rated-9, …) + rating_cls = \ + (li.xpath(".//div[@class='film-detail-content']//span[contains(@class,'rating')]/@class") or [None])[0] + if rating_cls: + m = re.search(r"rated-(\d+)", rating_cls) + if m: + rating = int(m.group(1)) + + # Jahr aus Detail-Ansicht + year_txt = (li.xpath(".//div[@class='film-detail-content']/h2/small/a/text()") or [None])[0] + if year_txt and year_txt.isdigit(): + year = int(year_txt) + + # Fallback: Jahr aus data-item-full-display-name "Title (YYYY)" + if year is None: + full = node.get("data-item-full-display-name") or "" + m = re.search(r"\((\d{4})\)", full) + if m: + year = int(m.group(1)) + + items.append((letterboxd_id, slug, year, comment, rating)) + + # Nächste Seite: <a class="next">… oder <link rel="next" …> + next_url = (response.xpath("//a[contains(@class,'next')]/@href") + or response.xpath("//link[@rel='next']/@href")) + return items, next_url def _parse_list(self, list_url, limit, language): diff --git a/modules/library.py b/modules/library.py index 96f65f092..9188ab098 100644 --- a/modules/library.py +++ b/modules/library.py @@ -1,5 +1,8 @@ import os, time from abc import ABC, abstractmethod + +import requests + from modules import util from modules.meta import MetadataFile, OverlayFile from modules.operations import Operations @@ -9,7 +12,10 @@ logger = util.logger +MAX_IMAGE_SIZE = 10480000 # a little less than 10MB + class Library(ABC): + def __init__(self, config, params): self.session = None self.Radarr = None @@ -89,7 +95,7 @@ def __init__(self, config, params): self.overlay_artwork_quality = params["overlay_artwork_quality"] self.overlay_artwork_filetype = params["overlay_artwork_filetype"] self.assets_for_all = params["assets_for_all"] - self.assets_for_all_collections = False + self.assets_for_all_collections = params["assets_for_all_collections"] self.delete_collections = params["delete_collections"] self.mass_studio_update = params["mass_studio_update"] self.mass_genre_update = params["mass_genre_update"] @@ -126,7 +132,8 @@ def __init__(self, config, params): self.optimize = params["plex"]["optimize"] # TODO: Here or just in Plex? self.stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0, "names": []} self.status = {} - + self.EmbyServer=None + self.emby_server_url = None self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.remove_title_parentheses \ or self.mass_audience_rating_update or self.mass_critic_rating_update or self.mass_user_rating_update \ or self.mass_episode_audience_rating_update or self.mass_episode_critic_rating_update or self.mass_episode_user_rating_update \ @@ -200,7 +207,7 @@ def scan_files(self, operations_only, overlays_only, collection_only, metadata_o logger.info("") logger.separator(f"Skipping {e} Image File") - def upload_images(self, item, poster=None, background=None, overlay=False): + def upload_images(self, item, poster=None, background=None, logo=None, overlay=False): poster_uploaded = False if poster is not None: try: @@ -209,7 +216,7 @@ def upload_images(self, item, poster=None, background=None, overlay=False): _, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name) if not image_compare or str(poster.compare) != str(image_compare): if overlay: - self.reload(item, force=True) + # self.reload(item, force=True) if overlay and "Overlay" in [la.tag for la in self.item_labels(item)]: item.removeLabel("Overlay") poster_uploaded = self._upload_image(item, poster) @@ -234,13 +241,31 @@ def upload_images(self, item, poster=None, background=None, overlay=False): except Failed: logger.stacktrace() logger.error(f"Metadata: {background.attribute} failed to update {background.message}") + + logo_uploaded = False + if logo is not None: + try: + image_compare = None + if self.config.Cache: + _, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_logos") + if not image_compare or str(logo.compare) != str(image_compare): + logo_uploaded = self._upload_image(item, logo) + logger.info(f"Metadata: {logo.attribute} updated {logo.message}") + elif self.show_asset_not_needed: + logger.info(f"Metadata: {logo.prefix}logo update not needed") + except Failed: + logger.stacktrace() + logger.error(f"Metadata: {logo.attribute} failed to update {logo.message}") + if self.config.Cache: if poster_uploaded: self.config.Cache.update_image_map(item.ratingKey, self.image_table_name, "", poster.compare if poster else "") if background_uploaded: self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", "", background.compare) + if logo_uploaded: + self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_logos", "", logo.compare) - return poster_uploaded, background_uploaded + return poster_uploaded, background_uploaded, logo_uploaded def get_id_from_maps(self, key): key = int(key) @@ -265,6 +290,10 @@ def _upload_image(self, item, image): def upload_poster(self, item, image, url=False): pass + @abstractmethod + def upload_poster_overlay(self, item, image, url=False): + pass + def poster_update(self, item, image, tmdb=None, title=None): return self.image_update(item, image, tmdb=tmdb, title=title) @@ -275,8 +304,7 @@ def background_update(self, item, image, tmdb=None, title=None): def image_update(self, item, image, tmdb=None, title=None, poster=True): pass - def pick_image(self, title, images, prioritize_assets, download_url_assets, item_dir, is_poster=True, image_name=None): - image_type = "poster" if is_poster else "background" + def pick_image(self, title, images, prioritize_assets, download_url_assets, item_dir, image_type="poster", image_name=None): if image_name is None: image_name = image_type if images: @@ -293,17 +321,31 @@ def pick_image(self, title, images, prioritize_assets, download_url_assets, item "tmdb_producer_details", "tmdb_writer_details", "tmdb_movie_details", "tmdb_list_details", "tvdb_list_details", "tvdb_movie_details", "tvdb_show_details", "tmdb_show_details"]: if attr in images: + ok = True + if attr != f"file_{image_type}": + import urllib.request, urllib.error + try: + ok = urllib.request.urlopen(urllib.request.Request(images[attr], method='HEAD'), timeout=5).status != 404 + except urllib.error.HTTPError as e: + ok = e.code != 404 + except Exception: + ok = False + + if not ok: + continue + if attr in ["style_data", f"url_{image_type}"] and download_url_assets and item_dir: + requests.delete() if "asset_directory" in images: return images["asset_directory"] else: try: - return self.config.Requests.download_image(title, images[attr], item_dir, session=self.session, is_poster=is_poster, filename=image_name) + return self.config.Requests.download_image(title, images[attr], item_dir, session=self.session, image_type=image_type, filename=image_name) except Failed as e: logger.error(e) if attr in ["asset_directory", f"pmm_{image_type}"]: return images[attr] - return ImageData(attr, images[attr], is_poster=is_poster, is_url=attr != f"file_{image_type}") + return ImageData(attr, images[attr], image_type=image_type, is_url=attr != f"file_{image_type}") @abstractmethod def reload(self, item, force=False): @@ -342,6 +384,13 @@ def item_posters(self, item, providers=None): @abstractmethod def get_all(self, builder_level=None, load=False): pass + @abstractmethod + def get_native_emby_item(self, emby_item_id): + pass + + @abstractmethod + def get_all_native(self, builder_level=None, load=False): + pass def add_additions(self, collection, items, is_movie): self._add_to_file("Added", collection, items, is_movie) @@ -390,25 +439,43 @@ def cache_items(self): logger.separator(f"Caching {self.name} Library Items", space=False, border=False) logger.info("") items = self.get_all() + # print(items) for item in items: + # if item.get("ratingKey") is None: + # print(item) self.cached_items[item.ratingKey] = (item, False) return items + + @abstractmethod + def get_provider_ids(self, item): + pass + def map_guids(self, items): for i, item in enumerate(items, 1): + logger.ghost(f"Mapping: {i}/{len(items)}") if isinstance(item, tuple): - logger.ghost(f"Processing: {i}/{len(items)}") + # logger.ghost(f"Processing: {i}/{len(items)}") key, guid = item else: - logger.ghost(f"Processing: {i}/{len(items)} {item.title}") + # logger.ghost(f"Processing: {i}/{len(items)} {item.title}") + # print(item) key = item.ratingKey guid = item.guid if key not in self.movie_rating_key_map and key not in self.show_rating_key_map: + # print(key, guid) + self.config.Convert.scan_guid(guid) if isinstance(item, tuple): item_type, check_id = self.config.Convert.scan_guid(guid) id_type, main_id, imdb_id, _ = self.config.Convert.ids_from_cache(key, guid, item_type, check_id, self) else: - id_type, main_id, imdb_id = self.config.Convert.get_id(item, self) + mydata = self.get_provider_ids(item) + id_type, main_id, imdb_id = self.config.Convert.get_id(item, self, mydata) + + + # print(f" - {id_type} - {main_id} - {imdb_id}") + # 1145484 tt0076759 + # - movie - [] - ['0076759'] if main_id: if id_type == "movie": if len(main_id) > 1: @@ -445,3 +512,10 @@ def map_guids(self, items): self.reverse_mal[v] = k logger.info("") logger.info(f"Processed {len(items)} {self.type}s") + + def validate_image_size(self, image): + if image.compare < MAX_IMAGE_SIZE: + return True + else: + logger.error(f"Image too large: {image.location}, bytes {image.compare}, MAX {MAX_IMAGE_SIZE}") + return False \ No newline at end of file diff --git a/modules/logs.py b/modules/logs.py index e31fe16f4..eb1a5caf1 100644 --- a/modules/logs.py +++ b/modules/logs.py @@ -28,8 +28,10 @@ def fmt_filter(record): _srcfile = os.path.normcase(fmt_filter.__code__.co_filename) def log_namer(default_name): - base, ext, num = default_name.split(".") - return f"{base}-{num}.{ext}" + log_path = os.path.dirname(default_name) + log_file = os.path.basename(default_name) + base, ext, num = log_file.split(".") + return f"{log_path}/{base}-{num}.{ext}" class MyLogger: diff --git a/modules/mdblist.py b/modules/mdblist.py index dfa114046..1066c1126 100644 --- a/modules/mdblist.py +++ b/modules/mdblist.py @@ -150,6 +150,8 @@ def get_item(self, imdb_id=None, tmdb_id=None, tvdb_id=None, is_movie=True, igno mdb_dict, expired = self.cache.query_mdb(key, self.expiration) if mdb_dict and expired is False: return MDbObj(mdb_dict) + if self.limit: + raise Failed logger.trace(f"ID: {key}") mdb = MDbObj(self._request(api_url, params=params)) if self.cache and not ignore_cache: diff --git a/modules/meta.py b/modules/meta.py index e19a0ec05..2e825b23a 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -13,7 +13,7 @@ "trakt_liked_lists", "trakt_people_list", "subtitle_language", "audio_language", "resolution", "decade", "imdb_awards" ] auto = { - "Movie": ["tmdb_collection", "edition", "country", "director", "producer", "writer", "letterboxd_user_lists"] + all_auto + ms_auto, + "Movie": ["tmdb_collection", "edition", "country", "director", "producer", "writer", "composer", "letterboxd_user_lists"] + all_auto + ms_auto, "Show": ["network", "origin_country", "episode_year"] + all_auto + ms_auto, "Artist": ["mood", "style", "country", "album_genre", "album_mood", "album_style", "track_mood"] + all_auto, "Video": ["country", "content_rating"] + all_auto @@ -784,6 +784,7 @@ def __init__(self, config, library, file_type, path, temp_vars, asset_directory, self.collections = get_dict("collections", data, library.collections) self.dynamic_collections = get_dict("dynamic_collections", data) col_names = library.collections + [c for c in self.collections] + local_people_cache = {} for map_name, dynamic in self.dynamic_collections.items(): logger.info("") logger.separator(f"Building {map_name} Dynamic Collections", space=False, border=False) @@ -954,7 +955,7 @@ def _check_dict(check_dict): auto_list[country.iso_3166_1.lower()] = country.name logger.exorcise() default_title_format = "<<key_name>> <<library_type>>s" - elif auto_type in ["actor", "director", "writer", "producer"]: + elif auto_type in ["actor", "director", "writer", "producer", "composer"]: people = {} dynamic_data = util.parse("Config", "data", dynamic, parent=map_name, methods=methods, datatype="dict") if "data" in self.temp_vars: @@ -971,20 +972,69 @@ def _check_dict(check_dict): person_depth = util.parse("Config", "depth", dynamic_data, parent=f"{map_name} data", methods=person_methods, datatype="int", default=3, minimum=1) person_minimum = util.parse("Config", "minimum", dynamic_data, parent=f"{map_name} data", methods=person_methods, datatype="int", default=3, minimum=1) if "minimum" in person_methods else None person_limit = util.parse("Config", "limit", dynamic_data, parent=f"{map_name} data", methods=person_methods, datatype="int", default=25, minimum=1) if "limit" in person_methods else None - lib_all = library.get_all() + lib_all = library.get_all_native() include_cols = [] for i, item in enumerate(lib_all, 1): - logger.ghost(f"Scanning: {i}/{len(lib_all)} {item.title}") + logger.ghost(f"Scanning: {i}/{len(lib_all)} {item.get('Name')}") + my_emby_cast = None try: - item = self.library.reload(item) - for person in getattr(item, f"{auto_type}s")[:person_depth]: - if person.tag in include: - if person.tag not in include_cols: - include_cols.append(person.tag) + if item.get('Id') in local_people_cache: + my_emby_cast, emby_people = local_people_cache.get(item.get('Id')) + else: + emby_people = item.get('People', []) + if dynamic.get("template") and "tmdb_person" in dynamic["template"]: + emby_ids = [entry.get("Id") for entry in emby_people] + my_emby_cast = self.library.EmbyServer.get_items_bulk(emby_ids, ["ProviderIds"]) + local_people_cache[item.get('Id')] = (my_emby_cast, emby_people) + + # Director, Writer + the_list = [] + match auto_type: + case "actor": + actors = [person for person in emby_people if person.get('Type') == 'Actor'] + the_list = actors + case "director": + director = [person for person in emby_people if + person.get('Type') == 'Director'] + the_list = director + case "writer": + writers = [person for person in emby_people if + person.get('Type') == 'Writer'] + the_list = writers + case "composer": + composers = [person for person in emby_people if + person.get('Type') == 'Composer'] + the_list = composers + case "producer": + producers = [person for person in emby_people if person.get('Type') == 'Producer'] + the_list = producers + case _: + logger.debug(f"{auto_type} - Missing for People. Please inform the developer.") + for person in the_list[:person_depth]: + if my_emby_cast: + emby_id = person.get('Id') + e_person = my_emby_cast.get(emby_id) + e_tmdbid = e_person["ProviderIds"].get("Tmdb") or e_person["ProviderIds"].get("tmdb") + if not e_tmdbid: + # logger.info(f"Tmdb id missing for '{person.get("Name")}' - '{emby_id}'")# ToDo => people scraping + continue + if e_tmdbid in include: + if e_tmdbid not in include_cols: + include_cols.append(e_tmdbid) + else: + if int(emby_id) not in people: + people[int(emby_id)] = {"name": person.get('Name'),"tmdb_person_id":e_tmdbid, + "count": 0} + people[int(emby_id)]["count"] += 1 else: - if person.id not in people: - people[person.id] = {"name": person.tag, "count": 0} - people[person.id]["count"] += 1 + if person.get('Name') in include: + if person.get('Name') not in include_cols: + include_cols.append(person.get('Name')) + else: + if int(person.get('Id')) not in people: + people[int(person.get('Id'))] = {"name": person.get('Name'), "count": 0} + people[int(person.get('Id'))]["count"] += 1 + except Failed as e: logger.error(f"Plex Error: {e}") roles = [data for _, data in people.items()] @@ -1000,8 +1050,12 @@ def _check_dict(check_dict): person_count += 1 for role in roles: if person_count < person_limit and role["count"] >= person_minimum and role["name"] not in exclude: - auto_list[role["name"]] = role["name"] - all_keys[role["name"]] = role["name"] + if my_emby_cast: + auto_list[role["tmdb_person_id"]] = role["name"] + all_keys[role["tmdb_person_id"]] = role["name"] + else: + auto_list[role["name"]] = role["name"] + all_keys[role["name"]] = role["name"] person_count += 1 default_template = {"plex_search": {"any": {auto_type: "<<value>>"}}} elif auto_type == "imdb_awards": @@ -1014,8 +1068,8 @@ def _check_dict(check_dict): event_id = util.parse("Config", "event_id", dynamic_data, parent=f"{map_name} data", methods=award_methods, regex=(r"(ev\d+)", "ev0000003")) increment = util.parse("Config", "increment", dynamic_data, parent=f"{map_name} data", methods=award_methods, datatype="int", default=1, minimum=1) if "increment" in award_methods else 1 extra_template_vars["event_id"] = event_id - if event_id not in self.config.IMDb.events_validation: - raise Failed(f"Config Error: {map_name} data only specific Event IDs work with imdb_awards. Event Options: [{', '.join([k for k in self.config.IMDb.events_validation])}]") + if event_id not in self.config.IMDb.git_events_validation: + raise Failed(f"Config Error: {map_name} data only specific Event IDs work with imdb_awards. Event Options: [{', '.join([k for k in self.config.IMDb.git_events_validation])}]") _, event_years = self.config.IMDb.get_event_years(event_id) year_options = [event_years[len(event_years) - i] for i in range(1, len(event_years) + 1)] @@ -1208,6 +1262,7 @@ def get_position(attr): if "<<library_type>>" in title_format: title_format = title_format.replace("<<library_type>>", library.type.lower()) if "<<library_typeU>>" in title_format: + # print(library.type) title_format = title_format.replace("<<library_typeU>>", library.type) if "limit" in self.temp_vars and "<<limit>>" in title_format: title_format = title_format.replace("<<limit>>", str(self.temp_vars["limit"])) @@ -1270,6 +1325,10 @@ def get_position(attr): logger.debug(f"Test: {test}") logger.debug(f"Sync: {sync}") logger.debug(f"Include: {include}") + + if other_name: + other_name = other_name.replace("<<library_typeU>>", library.type) + logger.debug(f"Other Name: {other_name}") logger.debug(f"All Keys: {all_keys.keys()}") if not auto_list: @@ -1411,6 +1470,7 @@ def from_repo(u): def check_for_definition(check_key, check_tree, is_poster=True, git_name=None): attr_name = "poster" if is_poster and (git_name is None or "background" not in git_name) else "background" + # print(attr_name) if (git_name and git_name.lower().endswith(".tpdb")) or (not git_name and f"{attr_name}.tpdb" in check_tree): return f"tpdb_{attr_name}", from_repo(f"{check_key}/{quote(git_name) if git_name else f'{attr_name}.tpdb'}") elif (git_name and git_name.lower().endswith(".url")) or (not git_name and f"{attr_name}.url" in check_tree): @@ -1778,9 +1838,12 @@ def add_edit(name, current_item, group=None, alias=None, key=None, value=None, v final_value = value if current != str(final_value): if key == "title": - current_item.editTitle(final_value) + # current_item.editTitle(final_value) + self.library.EmbyServer.editItemTitle(current_item.ratingKey, final_value) else: - current_item.editField(key, final_value) + # current_item.editField(key, final_value) + self.library.EmbyServer.editItemField(current_item.ratingKey, final_value) + logger.info(f"Metadata: {name} updated to {final_value}") updated = True except Failed as ee: @@ -2188,7 +2251,7 @@ def finish_edit(current_item, description): else: logger.error(f"{self.type_str} Error: f1_language must be a language code Kometa has a translation for. Options: {ergast.translations}") logger.info(f"Setting {item.title} of {self.type_str} to F1 Season {f1_season}") - races = self.config.Ergast.get_races(f1_season, f1_language) + races = self.config.Ergast.get_races(f1_season, f1_language, round_prefix, shorten_gp) race_lookup = {r.round: r for r in races} logger.trace(race_lookup) for season in item.seasons(): @@ -2201,11 +2264,10 @@ def finish_edit(current_item, description): break if season.seasonNumber in race_lookup: race = race_lookup[season.seasonNumber] - title = race.format_name(round_prefix, shorten_gp) updated = False - add_edit("title", season, value=title) - finish_edit(season, f"Season: {title}") - _, _, ups = self.library.item_images(season, {}, {}, asset_location=asset_location, title=title, + add_edit("title", season, value=race.title) + finish_edit(season, f"Season: {race.title}") + _, _, ups = self.library.item_images(season, {}, {}, asset_location=asset_location, title=race.title, image_name=f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}", folder_name=folder_name) if ups: updated = True @@ -2323,7 +2385,7 @@ def __init__(self, config, library, file_type, path, temp_vars, asset_directory, break if not condition_found: defaults[con_key] = con_value["default"] - if "dynamic_position" in queue["settings"] and queue["settings"]["dynamic_position"] and isinstance(queue["settings"]["dynamic_position"], dict): + if "dynamic_position" in queue["settings"] and queue["settings"]["dynamic_position"] and isinstance(queue["settings"]["dynamic_position"], dict) and not queue_position: dynamic_settings = { "initial_vertical_align": None, "initial_horizontal_align": None, "surround": False, "initial_vertical_offset": 0, "initial_horizontal_offset": 0, "vertical_spacing": 0, "horizontal_spacing": 0 diff --git a/modules/mojo.py b/modules/mojo.py index 61a7e747a..d5d94c70c 100644 --- a/modules/mojo.py +++ b/modules/mojo.py @@ -260,7 +260,7 @@ def get_imdb_ids(self, method, data): imdb_id = None expired = None if self.cache: - imdb_id, expired = self.cache.query_letterboxd_map(item) + imdb_id, expired = self.cache.query_mojo_map(item) if not imdb_id or expired is not False: try: imdb_id = self._imdb(item) @@ -268,7 +268,7 @@ def get_imdb_ids(self, method, data): logger.error(e) continue if self.cache: - self.cache.update_letterboxd_map(expired, item, imdb_id) + self.cache.update_mojo_map(expired, item, imdb_id) ids.append((imdb_id, "imdb")) logger.info(f"Processed {total_items} IMDb IDs") return ids diff --git a/modules/operations.py b/modules/operations.py index 30f7416f9..2f757c79b 100644 --- a/modules/operations.py +++ b/modules/operations.py @@ -1,5 +1,6 @@ import os, re from datetime import datetime, timedelta, timezone + from modules import plex, util, anidb from modules.util import Failed, LimitReached from plexapi.exceptions import NotFound @@ -100,7 +101,9 @@ def should_be_deleted(col_in, labels_in, configured_in, managed_in, less_in): for i, track in enumerate(tracks, 1): logger.ghost(f"Processing Track: {i}/{len(tracks)} {track.title}") if not track.title and track.titleSort: - track.editTitle(track.titleSort) + # track.editTitle(track.titleSort) + self.library.EmbyServer.editItemTitle(track.ratingKey, track.titleSort) + num_edited += 1 logger.info(f"Track: {track.titleSort} was updated with sort title") logger.info(f"{len(tracks)} Tracks Processed; {num_edited} Blank Track Titles Updated") @@ -128,15 +131,46 @@ def should_be_deleted(col_in, labels_in, configured_in, managed_in, less_in): if self.library.assets_for_all and not self.library.asset_directory: logger.error("Asset Error: No Asset Directory for Assets For All") + # emby_people = EmbyPeopleSyncMixin(config=self.config,library=self.library) + people_alias_revert = [] + total_items = len(items) for i, item in enumerate(items, 1): logger.info("") - logger.info(f"Processing: {i}/{len(items)} {item.title}") - try: - item = self.library.reload(item) - except Failed as e: - logger.error(e) - continue - current_labels = [la.tag for la in self.library.item_labels(item)] if self.library.label_operations else [] + logger.info(f"({i}/{total_items}) {item.title}") + # try: + # item = self.library.reload(item) + # except Failed as e: + # logger.error(e) + # continue + + # Debugging + import time + _timer0 = time.perf_counter() + time_now = lambda: int((time.perf_counter() - _timer0) * 1000) # ms seit Start + + def time_reset(): + """Startpunkt neu setzen (optional).""" + global _timer0 + _timer0 = time.perf_counter() + + def tick(label: str, min_ms: int = 0): + """Schneller Log-Helfer (optional).""" + ms = time_now() + if ms >= min_ms: + try: + logger.info(f"[TIMER] {label}: +{ms} ms") + except Exception: + print(f"[TIMER] {label}: +{ms} ms") + # Debugging end + + emby_item = self.library.EmbyServer.get_item( + item.ratingKey) + + current_labels = [la.tag for la in + self.library.item_labels(item)] if self.library.label_operations else [] + item_genres = (emby_item.get("Genres", []) if emby_item else []) + + # tick("Received Emby item", min_ms=5) if self.library.assets_for_all and self.library.asset_directory: self.library.find_and_upload_assets(item, current_labels) @@ -144,16 +178,20 @@ def should_be_deleted(col_in, labels_in, configured_in, managed_in, less_in): locked_fields = [f.name for f in item.fields if f.locked] tmdb_id, tvdb_id, imdb_id = self.library.get_ids(item) + # tick("Fetched ids", min_ms=5) - item_edits = "" + item_edits = [] + do_cast_update = True if self.library.remove_title_parentheses: if not any([f.name == "title" and f.locked for f in item.fields]) and item.title.endswith(")"): new_title = re.sub(" \\(\\w+\\)$", "", item.title) - item.editTitle(new_title) - item_edits += f"\nUpdated Title: {item.title[:25]:<25} | {new_title}" + # item.editTitle(new_title) + self.library.EmbyServer.editItemTitle(item.ratingKey, new_title) + item_edits.append(f"\nUpdated Title: {item.title[:25]:<25} | {new_title}") if self.library.mass_imdb_parental_labels: + # Emby: not tested try: if self.library.mass_imdb_parental_labels == "remove": parental_labels = [] @@ -168,10 +206,11 @@ def should_be_deleted(col_in, labels_in, configured_in, managed_in, less_in): if label not in label_edits[edit_type]: label_edits[edit_type][label] = [] label_edits[edit_type][label].append(item.ratingKey) - item_edits += f"\n{edit_type.capitalize()} IMDb Parental Labels (Batched) | {', '.join(label_list)}" + item_edits.append(f"\n{edit_type.capitalize()} IMDb Parental Labels (Batched) | {', '.join(label_list)}") except Failed: pass if item.locations: + # Emby: the location cannot be saved to my custom Embvy object, will need rework path = os.path.dirname(str(item.locations[0])) if self.library.is_movie else str(item.locations[0]) if self.library.Radarr and self.library.radarr_add_all_existing and tmdb_id: path = path.replace(self.library.Radarr.plex_path, self.library.Radarr.radarr_path) @@ -192,12 +231,12 @@ def trakt_ratings(): return _trakt_ratings _tmdb_obj = None - def tmdb_obj(): + def tmdb_obj(ignore_cache = False): nonlocal _tmdb_obj if _tmdb_obj is None: _tmdb_obj = False try: - _item = self.config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=self.library.is_movie) + _item = self.config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=self.library.is_movie, ignore_cache=ignore_cache ) if _item: _tmdb_obj = _item except Failed as err: @@ -234,7 +273,7 @@ def tvdb_obj(): _tvdb_obj = False if tvdb_id: try: - _tvdb_obj = self.config.TVDb.get_tvdb_obj(tvdb_id, is_movie=self.library.is_movie) + _tvdb_obj = self.config.TVDb.get_tvdb_obj_from_id(tvdb_id, is_movie=self.library.is_movie) except Failed as err: logger.error(str(err)) else: @@ -248,39 +287,39 @@ def mdb_obj(): nonlocal _mdb_obj if _mdb_obj is None: _mdb_obj = False - if self.config.MDBList.limit is False: - if self.library.is_show and tvdb_id: - try: - _mdb_obj = self.config.MDBList.get_series(tvdb_id) - except LimitReached as err: - logger.debug(err) - except Failed as err: - logger.error(str(err)) - except Exception: - logger.trace(f"TVDb ID: {tvdb_id}") - raise - if self.library.is_movie and tmdb_id: - try: - _mdb_obj = self.config.MDBList.get_movie(tmdb_id) - except LimitReached as err: - logger.debug(err) - except Failed as err: - logger.error(str(err)) - except Exception: - logger.trace(f"TMDb ID: {tmdb_id}") - raise - if imdb_id and not _mdb_obj: - try: - _mdb_obj = self.config.MDBList.get_imdb(imdb_id) - except LimitReached as err: - logger.debug(err) - except Failed as err: - logger.error(str(err)) - except Exception: - logger.trace(f"IMDb ID: {imdb_id}") - raise - if not _mdb_obj: - logger.warning(f"No MdbItem for {item.title} (Guid: {item.guid})") + # if self.config.MDBList.limit is False: + if self.library.is_show and tvdb_id: + try: + _mdb_obj = self.config.MDBList.get_series(tvdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"TVDb ID: {tvdb_id}") + raise + if self.library.is_movie and tmdb_id: + try: + _mdb_obj = self.config.MDBList.get_movie(tmdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"TMDb ID: {tmdb_id}") + raise + if imdb_id and not _mdb_obj: + try: + _mdb_obj = self.config.MDBList.get_imdb(imdb_id) + except LimitReached as err: + logger.debug(err) + except Failed as err: + logger.error(str(err)) + except Exception: + logger.trace(f"IMDb ID: {imdb_id}") + raise + if not _mdb_obj: + logger.warning(f"No MdbItem for {item.title} (Guid: {item.guid})") if not _mdb_obj: raise Failed return _mdb_obj @@ -334,6 +373,7 @@ def mal_obj(): raise Failed return _mal_obj + for attribute, item_attr in [ (self.library.mass_audience_rating_update, "audienceRating"), (self.library.mass_critic_rating_update, "rating"), @@ -347,24 +387,24 @@ def mal_obj(): if item_attr not in remove_edits: remove_edits[item_attr] = [] remove_edits[item_attr].append(item.ratingKey) - item_edits += f"\nRemove {name_display[item_attr]} (Batched)" + item_edits.append(f"Remove {name_display[item_attr]} (Batched)") elif item_attr not in locked_fields: if item_attr not in lock_edits: lock_edits[item_attr] = [] lock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nLock {name_display[item_attr]} (Batched)" + item_edits.append(f"Lock {name_display[item_attr]} (Batched)") break elif option in ["unlock", "reset"]: if option == "reset" and current: if item_attr not in reset_edits: reset_edits[item_attr] = [] reset_edits[item_attr].append(item.ratingKey) - item_edits += f"\nReset {name_display[item_attr]} (Batched)" + item_edits.append(f"Reset {name_display[item_attr]} (Batched)") elif item_attr in locked_fields: if item_attr not in unlock_edits: unlock_edits[item_attr] = [] unlock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nUnlock {name_display[item_attr]} (Batched)" + item_edits.append(f"Unlock {name_display[item_attr]} (Batched)") break else: try: @@ -437,14 +477,20 @@ def mal_obj(): if found_rating not in rating_edits[item_attr]: rating_edits[item_attr][found_rating] = [] rating_edits[item_attr][found_rating].append(item.ratingKey) - item_edits += f"\nUpdate {name_display[item_attr]} (Batched) | {found_rating}" + item_edits.append(f"Update {name_display[item_attr]} (Batched) | {found_rating}") + do_cast_update = True break except Failed: continue + if self.library.mass_genre_update or self.library.genre_mapper: + + # Title and cast updates, time consuming, parked here till proper integration + new_genres = [] extra_option = None + # tick("Begin genre update", min_ms=5) if self.library.mass_genre_update: for option in self.library.mass_genre_update: if option in ["lock", "unlock", "remove", "reset"]: @@ -471,8 +517,11 @@ def mal_obj(): break except Failed: continue + # tick("Genres received", min_ms=5) + + # item_genres = [g.tag for g in item.genres] + # item_genres = self.library.EmbyServer.get_item(item.ratingKey).get("Genres", []) - item_genres = [g.tag for g in item.genres] if not new_genres and extra_option not in ["remove", "reset"]: new_genres = item_genres if self.library.genre_mapper: @@ -486,23 +535,33 @@ def mal_obj(): new_genres = mapped_genres _add = list(set(new_genres) - set(item_genres)) _remove = list(set(item_genres) - set(new_genres)) + + # Update genres without batch, as not supported in Emby + for genre_list, edit_type in [(_add, "add"), (_remove, "remove")]: if genre_list: for g in genre_list: if g not in genre_edits[edit_type]: genre_edits[edit_type][g] = [] genre_edits[edit_type][g].append(item.ratingKey) - item_edits += f"\n{edit_type.capitalize()} Genres (Batched) | {', '.join(genre_list)}" + item_edits.append(f"{edit_type.capitalize()} Genres (Batched) | {', '.join(genre_list)}") if extra_option in ["unlock", "reset"] and ("genre" in locked_fields or _add or _remove): if "genre" not in unlock_edits: unlock_edits["genre"] = [] unlock_edits["genre"].append(item.ratingKey) - item_edits += "\nUnlock Genre (Batched)" + item_edits.append("Unlock Genre (Batched)") elif extra_option in ["lock", "remove"] and "genre" not in locked_fields and not _add and not _remove: if "genre" not in lock_edits: lock_edits["genre"] = [] lock_edits["genre"].append(item.ratingKey) - item_edits += "\nLock Genre (Batched)" + item_edits.append("Lock Genre (Batched)") + + if new_genres != item_genres: + self.library.EmbyServer.set_genres(item.ratingKey, new_genres) + do_cast_update = True + + + # tick("Mass genre updated", min_ms=5) if self.library.mass_content_rating_update or self.library.content_rating_mapper: new_rating = None @@ -563,7 +622,7 @@ def mal_obj(): if "contentRating" not in reset_edits: reset_edits["contentRating"] = [] reset_edits["contentRating"].append(item.ratingKey) - item_edits += "\nReset Content Rating (Batched)" + item_edits.append("Reset Content Rating (Batched)") elif "contentRating" in locked_fields: do_unlock = True elif extra_option == "remove" or is_none: @@ -571,26 +630,27 @@ def mal_obj(): if "contentRating" not in remove_edits: remove_edits["contentRating"] = [] remove_edits["contentRating"].append(item.ratingKey) - item_edits += "\nRemove Content Rating (Batched)" + item_edits.append("Remove Content Rating (Batched)") elif "contentRating" not in locked_fields: do_lock = True elif new_rating and new_rating != current_rating: if new_rating not in content_edits: content_edits[new_rating] = [] content_edits[new_rating].append(item.ratingKey) - item_edits += f"\nUpdate Content Rating (Batched) | {new_rating}" + item_edits.append(f"Update Content Rating (Batched) | {new_rating}") do_lock = False if extra_option == "lock" or do_lock: if "contentRating" not in lock_edits: lock_edits["contentRating"] = [] lock_edits["contentRating"].append(item.ratingKey) - item_edits += "\nLock Content Rating (Batched)" + item_edits.append("\nLock Content Rating (Batched)") elif extra_option == "unlock" or do_unlock: if "contentRating" not in unlock_edits: unlock_edits["contentRating"] = [] unlock_edits["contentRating"].append(item.ratingKey) - item_edits += "\nUnlock Content Rating (Batched)" + item_edits.append("Unlock Content Rating (Batched)") + # tick("Rating updated", min_ms=5) if self.library.mass_original_title_update: current_original = item.originalTitle @@ -600,24 +660,24 @@ def mal_obj(): if "originalTitle" not in remove_edits: remove_edits["originalTitle"] = [] remove_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nRemove Original Title (Batched)" + item_edits.append("Remove Original Title (Batched)") elif "originalTitle" not in locked_fields: if "originalTitle" not in lock_edits: lock_edits["originalTitle"] = [] lock_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nLock Original Title (Batched)" + item_edits.append("Lock Original Title (Batched)") break elif option in ["unlock", "reset"]: if option == "reset" and current_original: if "originalTitle" not in reset_edits: reset_edits["originalTitle"] = [] reset_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nReset Original Title (Batched)" + item_edits.append("Reset Original Title (Batched)") elif "originalTitle" in locked_fields: if "originalTitle" not in unlock_edits: unlock_edits["originalTitle"] = [] unlock_edits["originalTitle"].append(item.ratingKey) - item_edits += "\nUnlock Original Title (Batched)" + item_edits.append("Unlock Original Title (Batched)") break else: try: @@ -638,7 +698,7 @@ def mal_obj(): raise Failed if str(current_original) != str(new_original_title): item.editOriginalTitle(new_original_title) - item_edits += f"\nUpdated Original Title | {new_original_title}" + item_edits.append(f"Updated Original Title | {new_original_title}") break except Failed: continue @@ -651,24 +711,24 @@ def mal_obj(): if "studio" not in remove_edits: remove_edits["studio"] = [] remove_edits["studio"].append(item.ratingKey) - item_edits += "\nRemove Studio (Batched)" + item_edits.append("\nRemove Studio (Batched)") elif "studio" not in locked_fields: if "studio" not in lock_edits: lock_edits["studio"] = [] lock_edits["studio"].append(item.ratingKey) - item_edits += "\nLock Studio (Batched)" + item_edits.append("\nLock Studio (Batched)") break elif option in ["unlock", "reset"]: if option == "reset" and current_studio: if "studio" not in reset_edits: reset_edits["studio"] = [] reset_edits["studio"].append(item.ratingKey) - item_edits += "\nReset Studio (Batched)" + item_edits.append("\nReset Studio (Batched)") elif "studio" in locked_fields: if "studio" not in unlock_edits: unlock_edits["studio"] = [] unlock_edits["studio"].append(item.ratingKey) - item_edits += "\nUnlock Studio (Batched)" + item_edits.append("\nUnlock Studio (Batched)") break else: try: @@ -687,10 +747,11 @@ def mal_obj(): if new_studio not in studio_edits: studio_edits[new_studio] = [] studio_edits[new_studio].append(item.ratingKey) - item_edits += f"\nUpdate Studio (Batched) | {new_studio}" + item_edits.append(f"Update Studio (Batched) | {new_studio}") break except Failed: continue + # tick("Studio updated", min_ms=5) for attribute, item_attr in [ (self.library.mass_originally_available_update, "originallyAvailableAt"), @@ -706,24 +767,24 @@ def mal_obj(): if item_attr not in remove_edits: remove_edits[item_attr] = [] remove_edits[item_attr].append(item.ratingKey) - item_edits += f"\nRemove {name_display[item_attr]} (Batched)" + item_edits.append(f"\nRemove {name_display[item_attr]} (Batched)") elif item_attr not in locked_fields: if item_attr not in lock_edits: lock_edits[item_attr] = [] lock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nLock {name_display[item_attr]} (Batched)" + item_edits.append(f"\nLock {name_display[item_attr]} (Batched)") break elif option in ["unlock", "reset"]: if option == "reset" and current: if item_attr not in reset_edits: reset_edits[item_attr] = [] reset_edits[item_attr].append(item.ratingKey) - item_edits += f"\nReset {name_display[item_attr]} (Batched)" + item_edits.append(f"\nReset {name_display[item_attr]} (Batched)") elif item_attr in locked_fields: if item_attr not in unlock_edits: unlock_edits[item_attr] = [] unlock_edits[item_attr].append(item.ratingKey) - item_edits += f"\nUnlock {name_display[item_attr]} (Batched)" + item_edits.append(f"\nUnlock {name_display[item_attr]} (Batched)") break else: try: @@ -751,21 +812,101 @@ def mal_obj(): if new_date not in date_edits[item_attr]: date_edits[item_attr][new_date] = [] date_edits[item_attr][new_date].append(item.ratingKey) - item_edits += f"\nUpdate {name_display[item_attr]} (Batched) | {new_date}" + item_edits.append(f"Update {name_display[item_attr]} (Batched) | {new_date}") break except Failed: continue + # tick("Finished", min_ms=5) + + if False: # do_cast_update and self.library.is_movie and tmdb_id: # mass_cast_and_crew_update + try: + tmdb_item = tmdb_obj() + except Failed: + tmdb_item = None + + if tmdb_item and emby_item is not None: + + def contains_non_latin(text: str, allow_greek=False, allow_cyrillic=False) -> bool: + """True, wenn der Text Zeichen enthält, die nicht in lateinischen/erlaubten Blöcken liegen.""" + import unicodedata + if not text: + return False + for ch in text: + cp = ord(ch) + # Basic Latin + Latin-1 + Latin Extended + if 0x0000 <= cp <= 0x024F or 0x1E00 <= cp <= 0x1EFF or 0x2C60 <= cp <= 0x2C7F or 0xA720 <= cp <= 0xA7FF or 0xAB30 <= cp <= 0xAB6F: + continue + # Ziffern, Leerzeichen, Satzzeichen + if ch.isdigit() or ch.isspace() or unicodedata.category(ch).startswith("P"): + continue + # Optional: Griechisch + if allow_greek and (0x0370 <= cp <= 0x03FF or 0x1F00 <= cp <= 0x1FFF): + continue + # Optional: Kyrillisch + if allow_cyrillic and ( + 0x0400 <= cp <= 0x04FF or 0x0500 <= cp <= 0x052F or 0x2DE0 <= cp <= 0x2DFF or 0xA640 <= cp <= 0xA69F): + continue + return True + return False + + tmdb_title = tmdb_item.title + emby_title = emby_item.get("Name") + + all_cast_string = "".join( + [c.get("name", "") for c in tmdb_item.cast]) if tmdb_item.cast else "" + if contains_non_latin(tmdb_title) or contains_non_latin(all_cast_string): + # # pass + # # # ToDo: need to ensure EN title if lang from config tmdb has no translation + _tmdb_obj = None + tmdb_item = tmdb_obj(ignore_cache=True) + tmdb_title = tmdb_item.title + + my_cast = tmdb_item.cast + my_crew = tmdb_item.crew + + # This will get the title in the language set in config -> tmdb + # with foreign titles, emby will only scrape the original title; + # as I don't speak Chinese, I want EN title + + if tmdb_id == "46043": + pass + + if tmdb_title != emby_title: + + if contains_non_latin(emby_title) and not contains_non_latin(tmdb_title): + edits = {"Name": tmdb_title} + self.library.EmbyServer.update_item(emby_item.get("Id"), edits) + item_edits.append(f"Changed title from '{emby_title}' to '{tmdb_title}'") + pass + # ToDo: needs good trigger + option + if my_cast: + # try: + # has_edits, people_edits = emby_people.sync_people(emby_item, my_cast, my_crew) + + + has_edits, people_edits, new_people_renames = self.library.EmbyServer.sync_people(emby_item, my_cast, my_crew) + if has_edits: + item_edits.append(people_edits) + if new_people_renames: + people_alias_revert.extend(new_people_renames) + + # except Exception as e: + # logger.error(e) + # pass + # Title and case updates end + + # tick("Emby genres updated", min_ms=5) if len(item_edits) > 0: - logger.info(f"Item Edits{item_edits}") + logger.info(f"Item Edits: {"\n".join(item_edits)}") else: logger.info("No Item Edits") if self.library.mass_poster_update or self.library.mass_background_update: try: - new_poster, new_background, item_dir, name = self.library.find_item_assets(item) + new_poster, new_background, logo, item_dir, name = self.library.find_item_assets(item) except Failed: - new_poster, new_background, item_dir, name = None, None, None, None + new_poster, new_background, logo, item_dir, name = None, None, None, None, None try: tmdb_item = tmdb_obj() except Failed: @@ -776,7 +917,7 @@ def mal_obj(): ignore_locked = self.library.mass_poster_update["ignore_locked"] ignore_overlays = self.library.mass_poster_update.get("ignore_overlays") thumb_locked = any(f.name == "thumb" and f.locked for f in item.fields) - labels = [la.tag for la in self.library.item_labels(item)] + labels = current_labels # [la.tag for la in self.library.item_labels(item)] has_overlay_label = "Overlay" in labels # Bypass ignore_locked and ignore_overlays checks if the source is "unlock" or "lock" @@ -820,7 +961,7 @@ def mal_obj(): if (self.library.mass_poster_update and self.library.mass_poster_update["seasons"]) or \ (self.library.mass_background_update and self.library.mass_background_update["seasons"]): try: - season_poster, season_background, _, _ = self.library.find_item_assets(season, item_asset_directory=item_dir, folder_name=name) + season_poster, season_background, _, _, _ = self.library.find_item_assets(season, item_asset_directory=item_dir, folder_name=name) except Failed: season_poster = None season_background = None @@ -849,7 +990,7 @@ def mal_obj(): logger.error(f"S{season.seasonNumber}E{episode.episodeNumber} {episode.title} Failed to Reload from Plex") continue try: - episode_poster, episode_background, _, _ = self.library.find_item_assets(episode, item_asset_directory=item_dir, folder_name=name) + episode_poster, episode_background, _, _, _ = self.library.find_item_assets(episode, item_asset_directory=item_dir, folder_name=name) except Failed: episode_poster = None episode_background = None @@ -873,10 +1014,9 @@ def mal_obj(): for ep in item.episodes(): ep = self.library.reload(ep) - item_title = self.library.get_item_display_title(ep) + tmdb_title = self.library.get_item_display_title(ep) logger.info("") - logger.info(f"Processing {item_title}") - item_edits = "" + logger.info(f"Processing {tmdb_title}") for attribute, item_attr in episode_ops: if attribute: @@ -887,24 +1027,24 @@ def mal_obj(): if item_attr not in ep_remove_edits: ep_remove_edits[item_attr] = [] ep_remove_edits[item_attr].append(ep) - item_edits += f"\nRemove {name_display[item_attr]} (Batched)" + item_edits.append(f"\nRemove {name_display[item_attr]} (Batched)") elif item_attr not in locked_fields: if item_attr not in ep_lock_edits: ep_lock_edits[item_attr] = [] ep_lock_edits[item_attr].append(ep) - item_edits += f"\nLock {name_display[item_attr]} (Batched)" + item_edits.append(f"\nLock {name_display[item_attr]} (Batched)") break elif option in ["unlock", "reset"]: if option == "reset" and current: if item_attr not in ep_reset_edits: ep_reset_edits[item_attr] = [] ep_reset_edits[item_attr].append(ep) - item_edits += f"\nReset {name_display[item_attr]} (Batched)" + item_edits.append(f"\nReset {name_display[item_attr]} (Batched)") elif item_attr in locked_fields: if item_attr not in ep_unlock_edits: ep_unlock_edits[item_attr] = [] ep_unlock_edits[item_attr].append(ep) - item_edits += f"\nUnlock {name_display[item_attr]} (Batched)" + item_edits.append(f"\nUnlock {name_display[item_attr]} (Batched)") break else: try: @@ -934,20 +1074,22 @@ def mal_obj(): except ValueError: pass if not found_rating: - logger.info(f"No {option} {name_display[item_attr]} Found") + logger.info(f" No {option} {name_display[item_attr]} Found") raise Failed found_rating = f"{float(found_rating):.1f}" if str(current) != found_rating: if found_rating not in ep_rating_edits[item_attr]: ep_rating_edits[item_attr][found_rating] = [] ep_rating_edits[item_attr][found_rating].append(ep) - item_edits += f"\nUpdate {name_display[item_attr]} (Batched) | {found_rating}" + item_edits.append(f"Update {name_display[item_attr]} (Batched) | {found_rating}") break except Failed: continue if len(item_edits) > 0: - logger.info(f"Item Edits:{item_edits}") + logger.info(f"Item Edits: {"\n".join(item_edits)}") + + logger.info("") logger.separator("Batch Updates", space=False, border=False) @@ -960,36 +1102,97 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No f"{total_count} {'Episode' if is_episode else 'Movie' if self.library.is_movie else 'Show'}" \ f"{'s' if total_count > 1 else ''}{'' if out_type or tag_type else f' updated to {display_value}'}" - for tag_attribute, edit_dict in [("Label", label_edits), ("Genre", genre_edits)]: + # Genres set directly + # ToDO: label_edits currently not supported + for tag_attribute, edit_dict in [("Label", label_edits)]: #, ("Genre", genre_edits)]: + emby_item_edits = {} for edit_type, batch_edits in edit_dict.items(): _size = len(batch_edits.items()) for i, (tag_name, rating_keys) in enumerate(sorted(batch_edits.items()), 1): logger.info(get_batch_info(i, _size, tag_attribute, len(rating_keys), display_value=tag_name, tag_type=edit_type)) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - getattr(self.library.Plex, f"{edit_type}{tag_attribute}")(tag_name) - self.library.Plex.saveMultiEdits() + for r_key in rating_keys: + if r_key not in emby_item_edits: + base_data = self.library.EmbyServer.get_item(r_key).get("Genres",[]) + emby_item_edits.update({r_key:base_data}) + if edit_type == "add": + emby_item_edits[r_key].append(tag_name) + elif edit_type == "remove": + emby_item_edits[r_key].remove(tag_name) + + + + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # getattr(self.library.Plex, f"{edit_type}{tag_attribute}")(tag_name) + # self.library.Plex.saveMultiEdits() + # todo: add proper sync / collecting Emby item changes per ItemId + # for key, data in emby_item_edits.items(): + # print(".", end="", flush=True) + # self.library.EmbyServer.set_genres(key, data ) + + current_num = 1 for item_attr, _edits in rating_edits.items(): _size = len(_edits.items()) for i, (new_rating, rating_keys) in enumerate(sorted(_edits.items()), 1): - logger.info(get_batch_info(i, _size, item_attr, len(rating_keys), display_value=new_rating)) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex.editField(item_attr, new_rating) - self.library.Plex.saveMultiEdits() + logger.info(get_batch_info(current_num, _size, item_attr, len(rating_keys), display_value=new_rating)) + current_num+=1 + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex.editField(item_attr, new_rating) + # self.library.Plex.saveMultiEdits() + # fake edit + self.library.EmbyServer.multiEditField(self.library.load_list_from_cache(rating_keys),item_attr, new_rating) + # emby_changes = {} + # emby_changes.update(self.library.EmbyServer.multiEditRatings(rating_edits)) + + if people_alias_revert and len(people_alias_revert) > 1: + dedup = [] + seen = set() # Key pro Aktion + for emby_pid, (alias_name, clean_name, tid) in people_alias_revert: + key = (str(emby_pid), str(alias_name)) + if key in seen: + continue + seen.add(key) + dedup.append((str(emby_pid), (alias_name, clean_name, tid))) + + # 2) Stabil sortieren (erst nach alias, dann PID), damit das Log hübsch ist + dedup.sort(key=lambda t: (t[1][0].casefold(), t[0])) + _size = len(dedup) + else: + _size = len(people_alias_revert) + dedup = people_alias_revert + + for i, (emby_pid, (alias_name, clean_name, tid)) in enumerate(dedup, 1): + try: + logger.info(f"Reverting People Name {i}/{_size} | {alias_name} -> {clean_name}") + self.library.EmbyServer.update_item(emby_pid, + {"Id": emby_pid, "Name": clean_name, + "ProviderIds": {"Tmdb": tid}}) + # log["aliases_reverted"].append((emby_pid, clean_name)) + except Exception: + pass + _size = len(content_edits.items()) for i, (new_rating, rating_keys) in enumerate(sorted(content_edits.items()), 1): logger.info(get_batch_info(i, _size, "contentRating", len(rating_keys), display_value=new_rating)) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex.editContentRating(new_rating) - self.library.Plex.saveMultiEdits() + + self.library.EmbyServer.multiEditField(self.library.load_list_from_cache(rating_keys), "contentRating",new_rating) + + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex.editContentRating(new_rating) + # self.library.Plex.saveMultiEdits() _size = len(studio_edits.items()) for i, (new_studio, rating_keys) in enumerate(sorted(studio_edits.items()), 1): logger.info(get_batch_info(i, _size, "studio", len(rating_keys), display_value=new_studio)) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex.editStudio(new_studio) - self.library.Plex.saveMultiEdits() + rkeys = self.library.load_list_from_cache(rating_keys) + + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex.editStudio(new_studio) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multiEditField(self.library.load_list_from_cache(rating_keys), "studio", + new_studio) + _size = len(date_edits["originallyAvailableAt"].items()) for i, (new_date, rating_keys) in enumerate(sorted(date_edits["originallyAvailableAt"].items()), 1): @@ -1016,53 +1219,67 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No _size = len(remove_edits.items()) for i, (field_attr, rating_keys) in enumerate(remove_edits.items(), 1): - logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="remov")) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex.editField(field_attr, None, locked=True) - self.library.Plex.saveMultiEdits() + logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="remove")) + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex.editField(field_attr, None, locked=True) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multiEditField(rating_keys,field_attr, None, locked=True) + _size = len(reset_edits.items()) for i, (field_attr, rating_keys) in enumerate(reset_edits.items(), 1): logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="reset")) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex.editField(field_attr, None, locked=False) - self.library.Plex.saveMultiEdits() + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex.editField(field_attr, None, locked=False) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multiEditField(rating_keys,field_attr, None, locked=False) + _size = len(lock_edits.items()) for i, (field_attr, rating_keys) in enumerate(lock_edits.items(), 1): - logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="lock")) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex._edit(**{f"{field_attr}.locked": 1}) - self.library.Plex.saveMultiEdits() + # logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="lock")) + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex._edit(**{f"{field_attr}.locked": 1}) + # self.library.Plex.saveMultiEdits() + + self.library.EmbyServer.multi_edit(rating_keys, **{f"{field_attr}.locked": 1}) + _size = len(unlock_edits.items()) for i, (field_attr, rating_keys) in enumerate(unlock_edits.items(), 1): logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), out_type="unlock")) - self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) - self.library.Plex._edit(**{f"{field_attr}.locked": 0}) - self.library.Plex.saveMultiEdits() + # self.library.Plex.batchMultiEdits(self.library.load_list_from_cache(rating_keys)) + # self.library.Plex._edit(**{f"{field_attr}.locked": 0}) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multi_edit(rating_keys, **{f"{field_attr}.locked": 0}) for item_attr, _edits in ep_rating_edits.items(): _size = len(_edits.items()) for i, (new_rating, rating_keys) in enumerate(sorted(_edits.items()), 1): logger.info(get_batch_info(i, _size, item_attr, len(rating_keys), display_value=new_rating, is_episode=True)) - self.library.Plex.batchMultiEdits(rating_keys) - self.library.Plex.editField(item_attr, new_rating) - self.library.Plex.saveMultiEdits() + # self.library.Plex.batchMultiEdits(rating_keys) + # self.library.Plex.editField(item_attr, new_rating) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multiEditField(rating_keys,item_attr, new_rating) + _size = len(ep_remove_edits.items()) for i, (field_attr, rating_keys) in enumerate(ep_remove_edits.items(), 1): - logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), is_episode=True, out_type="remov")) - self.library.Plex.batchMultiEdits(rating_keys) - self.library.Plex.editField(field_attr, None, locked=True) - self.library.Plex.saveMultiEdits() + logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), is_episode=True, out_type="remove")) + # self.library.Plex.batchMultiEdits(rating_keys) + # self.library.Plex.editField(field_attr, None, locked=True) + # self.library.Plex.saveMultiEdits() + self.library.EmbyServer.multiEditField(rating_keys,field_attr, None, locked=True) + _size = len(ep_reset_edits.items()) for i, (field_attr, rating_keys) in enumerate(ep_reset_edits.items(), 1): logger.info(get_batch_info(i, _size, field_attr, len(rating_keys), is_episode=True, out_type="reset")) - self.library.Plex.batchMultiEdits(rating_keys) - self.library.Plex.editField(field_attr, None, locked=False) - self.library.Plex.saveMultiEdits() + # self.library.Plex.batchMultiEdits(rating_keys) + # self.library.Plex.editField(field_attr, None, locked=False) + # self.library.Plex.saveMultiEdits() + + self.library.EmbyServer.multiEditField(rating_keys,field_attr, None, locked=False) _size = len(ep_lock_edits.items()) for i, (field_attr, rating_keys) in enumerate(ep_lock_edits.items(), 1): @@ -1126,7 +1343,7 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No all_collections = self.library.get_all_collections() for i, col in enumerate(all_collections, 1): logger.ghost(f"Reading Collection: {i}/{len(all_collections)} {col.title}") - col = self.library.reload(col, force=True) + col = self.library.reload(col, force=True) # no reload with Emby labels = [la.tag for la in self.library.item_labels(col)] if should_be_deleted(col, labels, configured, managed, None if col.smart and ignore_smart else less): @@ -1173,7 +1390,7 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No logger.info("") for col in unconfigured_collections: try: - poster, background, item_dir, name = self.library.find_item_assets(col) + poster, background, _, item_dir, name = self.library.find_item_assets(col) if poster or background: self.library.upload_images(col, poster=poster, background=background) elif self.library.show_missing_assets: @@ -1220,6 +1437,7 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No if "year" in mv: special_names[f"{mv['title']} ({mv['year']})"] = mk items = self.library.get_all(load=True) + total_items = len(items) titles = [] year_titles = [] for item in items: @@ -1227,7 +1445,7 @@ def get_batch_info(placement, total, display_attr, total_count, display_value=No if isinstance(item, (Movie, Show)): year_titles.append(f"{item.title} ({item.year})") for i, item in enumerate(items, 1): - logger.ghost(f"Processing: {i}/{len(items)} {item.title}") + logger.ghost(f"({i}/{total_items}) {item.title}") map_key, attrs = self.library.get_locked_attributes(item, titles, year_titles) if map_key in special_names: map_key = special_names[map_key] diff --git a/modules/overlay.py b/modules/overlay.py index 69ed833b4..6c7f9a591 100644 --- a/modules/overlay.py +++ b/modules/overlay.py @@ -501,3 +501,6 @@ def get_canvas(self, item): else: canvas_size = portrait_dim return self.get_backdrop(canvas_size, box=self.backdrop_box, text=self.backdrop_text) + + def get_custom_canvas(self, canvas_size): + return self.get_backdrop(canvas_size, box=self.backdrop_box, text=self.backdrop_text) diff --git a/modules/overlays.py b/modules/overlays.py index 9e966569b..83792e649 100644 --- a/modules/overlays.py +++ b/modules/overlays.py @@ -2,6 +2,7 @@ from datetime import datetime from modules import plex, util, overlay from modules.builder import CollectionBuilder +from modules.poster import ImageData from modules.util import Failed, FilterFailed, NotScheduled, LimitReached from num2words import num2words from plexapi.exceptions import BadRequest @@ -30,6 +31,7 @@ def run_overlays(self): key_to_overlays, properties = self.compile_overlays() ignore_list = [rk for rk in key_to_overlays] + # I guess this block contains Plex legacy stuff old_overlays = [la for la in self.library.Plex.listFilterChoices("label") if str(la.title).lower().endswith(" overlay")] if old_overlays: logger.separator(f"Removing Old Overlays for the {self.library.name} Library") @@ -48,27 +50,29 @@ def run_overlays(self): ]) logger.info("") - remove_overlays = self.get_overlay_items(ignore=ignore_list) - if self.library.is_show: - remove_overlays.extend(self.get_overlay_items(libtype="episode", ignore=ignore_list)) - remove_overlays.extend(self.get_overlay_items(libtype="season", ignore=ignore_list)) - elif self.library.is_music: - remove_overlays.extend(self.get_overlay_items(libtype="album", ignore=ignore_list)) - - if remove_overlays: - logger.separator(f"Removing {'All ' if self.library.remove_overlays else ''}Overlays for the {self.library.name} Library") - for i, item in enumerate(remove_overlays, 1): - item_title = self.library.get_item_display_title(item) - logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item_title}") - self.remove_overlay(item, item_title, "Overlay", [ - os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png"), - os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg"), - os.path.join(self.library.overlay_backup, f"{item.ratingKey}.webp") - ]) - logger.exorcise() - else: - logger.separator(f"No Overlays to Remove for the {self.library.name} Library") - logger.info("") + # emby_image_manager = Emby_Image_Manager(self.library.name) + if False: # ToDo: Remove overlays not working as expected - deactiveted + remove_overlays = self.get_overlay_items(ignore=ignore_list) + if self.library.is_show: + remove_overlays.extend(self.get_overlay_items(libtype="episode", ignore=ignore_list)) + remove_overlays.extend(self.get_overlay_items(libtype="season", ignore=ignore_list)) + elif self.library.is_music: + remove_overlays.extend(self.get_overlay_items(libtype="album", ignore=ignore_list)) + + if remove_overlays: + logger.separator(f"Removing {'All ' if self.library.remove_overlays else ''}Overlays for the {self.library.name} Library") + for i, item in enumerate(remove_overlays, 1): + item_title = self.library.get_item_display_title(item) + logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item_title}") + self.remove_overlay(item, item_title, "Overlay", [ + os.path.join(self.library.overlay_destination_folder, f"{item}.png"), + os.path.join(self.library.overlay_destination_folder, f"{item}.jpg"), + os.path.join(self.library.overlay_destination_folder, f"{item}.webp") + ]) + logger.exorcise() + else: + logger.separator(f"No Overlays to Remove for the {self.library.name} Library") + logger.info("") if not self.library.remove_overlays: logger.separator(f"{'Re-' if self.library.reapply_overlays else ''}Applying Overlays for the {self.library.name} Library") logger.info("") @@ -82,19 +86,36 @@ def trakt_ratings(): raise Failed return _trakt_ratings + total_keys = len(key_to_overlays) for i, (over_key, (item, over_names)) in enumerate(sorted(key_to_overlays.items(), key=lambda io: self.library.get_item_display_title(io[1][0], sort=True)), 1): - item_title = self.library.get_item_display_title(item) + # item_title = self.library.get_item_display_title(item) + emby_item = self.library.EmbyServer.get_item(item.ratingKey) + emby_images = self.library.EmbyServer.get_item_images(item.ratingKey) + emby_poster = None + emby_thumb = None + if emby_images: + for image in emby_images: + match image.get('ImageType', None): + case 'Primary': + emby_poster = image + case 'Thumb': + emby_thumb = image + + item_title = emby_item.get("Name",'') try: - logger.ghost(f"Overlaying: {i}/{len(key_to_overlays)} {item_title}") + logger.ghost(f"Overlaying: ({i}/{total_keys}) {item_title}") image_compare = None overlay_compare = None poster = None if self.cache: image, image_compare, overlay_compare = self.cache.query_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays") - self.library.reload(item, force=True) + # self.library.reload(item, force=True) overlay_compare = [] if overlay_compare is None else util.get_list(overlay_compare, split="|") - has_overlay = any([item_tag.tag.lower() == "overlay" for item_tag in self.library.item_labels(item)]) + + my_overlay_path = os.path.join(self.library.overlay_destination_folder, f"{item.ratingKey}.{self.library.overlay_artwork_filetype}") + has_overlay = os.path.exists(my_overlay_path) + # has_overlay = any([item_tag.lower() == "overlay" for item_tag in self.library.item_labels(item)]) compare_names = {properties[ov].get_overlay_compare(): ov for ov in over_names} blur_num = 0 @@ -129,6 +150,7 @@ def trakt_ratings(): if self.cache: for over_name in over_names: + current_overlay = properties[over_name] # <--- WICHTIG if properties[over_name].name.startswith("text"): for cache_key, cache_value in self.cache.query_overlay_special_text(item.ratingKey).items(): actual = plex.attribute_translation[cache_key] if cache_key in plex.attribute_translation else cache_key @@ -151,7 +173,15 @@ def trakt_ratings(): if real_value != cache_value: overlay_change = f"Special Text Changed from {cache_value} to {real_value}" try: - poster, background, item_dir, name = self.library.find_item_assets(item) + #todo: for Emby transparent PNG, ignore existing poster files + poster = ImageData("asset_directory", my_overlay_path if has_overlay else "", is_url=False) + # poster = ImageData("asset_directory", emby_poster.get("Path"), is_url=False, compare=poster_compare) + # background = ImageData("asset_directory", emby_thumb.get("Path"), compare=emby_item.get("ImageTags").get("Thumb")) + item_dir = os.path.dirname(poster.location) + name = str(item_dir).split('\\')[-1] + + # poster, background, item_dir, name = self.library.find_item_assets(item) + if not poster and self.library.assets_for_all: if (isinstance(item, Episode) and self.library.show_missing_episode_assets) or \ (isinstance(item, Season) and self.library.show_missing_season_assets) or \ @@ -160,64 +190,39 @@ def trakt_ratings(): logger.warning(f"Asset Warning: No poster found for '{item_title}' in the assets folder '{item_dir}'") else: logger.warning(f"Asset Warning: No poster '{name}' found in the assets folders") - if background: - self.library.upload_images(item, background=background) + # if background: + # self.library.upload_images(item, background=background) except Failed as e: if self.library.assets_for_all and self.library.show_missing_assets: logger.warning(e) - has_original = None - new_backup = None changed_image = False + poster_compare = None if poster: - if image_compare and str(poster.compare) != str(image_compare): + poster_compare = poster.compare + if poster.compare and str(poster.compare) != str(image_compare): changed_image = True - if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")): - os.remove(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")) - if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")): - os.remove(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")) - if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.webp")): - os.remove(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.webp")) - elif has_overlay: - if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")): - has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png") - elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")): - has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") - elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.webp")): - has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.webp") - if self.library.reset_overlays: - reset_list = self.library.reset_overlays - elif has_original is None and not self.library.reset_overlays: - reset_list = ["plex", "tmdb"] - else: - reset_list = [] - try: - new_backup = self.library.item_posters(item, providers=reset_list) - except Failed as e: - if any(r in reset_list for r in ["plex", "tmdb"]): - logger.error(e) - else: - new_backup = item.posterUrl - logger.info(f"\n{item_title}") - if new_backup: - try: - has_original = self.library.check_image_for_overlay(new_backup, os.path.join(self.library.overlay_backup, f"{item.ratingKey}")) - except Failed as e: - raise Failed(f" Overlay Error: {e}") - poster_compare = None - if poster is None and has_original is None: - logger.error(f" Overlay Error: No poster found") - elif self.library.reapply_overlays or new_backup or overlay_change or changed_image: + + if self.library.reapply_overlays or overlay_change or changed_image: try: - if not self.library.reapply_overlays and new_backup: + if not self.library.reapply_overlays and changed_image: logger.trace(" Overlay Reason: New image detected") - elif not self.library.reapply_overlays and overlay_change: + if overlay_change: logger.trace(f" Overlay Reason: Overlay changed {overlay_change}") + + # canvas_width = emby_poster.get('Width', 0) + # canvas_height = emby_poster.get('Height',0) canvas_width, canvas_height = overlay.get_canvas_size(item) - with Image.open(poster.location if poster else has_original) as new_poster: + + # todo if filetype png / emby overlay + # canvas_width, canvas_height = overlay.get_canvas_size(item) + new_poster = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0)) + with (new_poster): + # with Image.open(poster.location if poster else has_original) as new_poster: exif_tags = new_poster.getexif() exif_tags[0x04bc] = "overlay" - new_poster = new_poster.convert("RGB").resize((canvas_width, canvas_height), Image.Resampling.LANCZOS) + # new_poster = new_poster.convert("RGBA").resize((canvas_width, canvas_height), Image.LANCZOS) + # new_poster = new_poster.convert("RGB").resize((canvas_width, canvas_height), Image.Resampling.LANCZOS) if blur_num > 0: new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num)) @@ -246,6 +251,7 @@ def get_text(text_overlay): actual_attr = format_var if format_var == "bitrate": actual_value = None + # ToDo for media in item.media: current = int(media.bitrate) if actual_value is None: @@ -414,10 +420,14 @@ def get_text(text_overlay): sub_items = [ep.duration for ep in sub_items if hasattr(ep, "duration") and ep.duration] actual_value = sum(sub_items) / len(sub_items) elif format_var == "total_runtime": + # ToDo sub_items = item.episodes() if text_overlay.level in ["show", "season"] else item.tracks() sub_items = [ep.duration for ep in sub_items if hasattr(ep, "duration") and ep.duration] actual_value = sum(sub_items) else: + # match actual_attr: + # case 'rating': + if not hasattr(item, actual_attr) or getattr(item, actual_attr) is None: raise Failed(f"Overlay Warning: No {full_text} found") actual_value = getattr(item, actual_attr) @@ -528,6 +538,7 @@ def get_text(text_overlay): else: overlay_box = current_overlay.get_coordinates((canvas_width, canvas_height), box=current_overlay.image.size, new_cords=cord) new_poster.paste(current_overlay.image, overlay_box, current_overlay.image) + # ext = "webp" if self.library.overlay_artwork_filetype.startswith("webp") else self.library.overlay_artwork_filetype ext = "webp" if self.library.overlay_artwork_filetype.startswith("webp") else self.library.overlay_artwork_filetype temp = os.path.join(self.library.overlay_folder, f"temp.{ext}") if self.library.overlay_artwork_quality and self.library.overlay_artwork_filetype in ["jpg", "webp_lossy"]: @@ -536,10 +547,14 @@ def get_text(text_overlay): new_poster.save(temp, exif=exif_tags, lossless=True) else: new_poster.save(temp, exif=exif_tags) - self.library.upload_poster(item, temp) - self.library.edit_tags("label", item, add_tags=["Overlay"], do_print=False) - poster_compare = poster.compare if poster else item.thumb - logger.info(f" Overlays Applied: {', '.join(over_names)}") + # self.library.upload_poster(item, temp) + self.library.upload_poster_overlay(item, temp) + # self.library.edit_tags("label", item, add_tags=["Overlay"], do_print=False) + # poster_compare = poster.compare if poster else item.thumb + + poster_compare = os.stat(temp).st_size # poster.compare if poster else item.thumb + + logger.info(f" Overlays Applied: {', '.join(over_names)}") except (OSError, BadRequest, SyntaxError) as e: logger.stacktrace() raise Failed(f" Overlay Error: {e}") @@ -658,35 +673,53 @@ def compile_overlays(self): key_to_overlays[over_key][1].append(overlay_name) for over_key, (item, over_names) in key_to_overlays.items(): - group_status = {} - for over_name in over_names: - for suppress_name in properties[over_name].suppress: - if suppress_name in over_names: - key_to_overlays[over_key][1].remove(suppress_name) - for over_name in over_names: - for overlay_group, group_names in overlay_groups.items(): - if over_name in group_names: - if overlay_group not in group_status: - group_status[overlay_group] = [] - group_status[overlay_group].append(over_name) - for gk, gv in group_status.items(): - if len(gv) > 1: - final = None - for v in gv: - if final is None or overlay_groups[gk][v] > overlay_groups[gk][final]: - final = v - for v in gv: - if final != v: - key_to_overlays[over_key][1].remove(v) + original = list(over_names) # Order beibehalten + + # -------- 1) SUPPRESS robust anwenden (keine In-Place-Removals) -------- + present = set(original) + to_remove = set() + for n in original: + # alle suppress-Ziele von n, die tatsächlich in der Liste sind + for s in properties[n].suppress: + if s in present: + to_remove.add(s) + + after_suppress = [n for n in original if n not in to_remove] + + # -------- 2) GROUP-Auswahl: je Gruppe nur den mit größtem Gewicht -------- + # overlay_groups: { group_name: {overlay_name: weight, ...}, ... } + drop_from_groups = set() + for group_name, weights in overlay_groups.items(): + # Kandidaten dieser Gruppe, die noch in der Liste sind (Order bleibt erhalten) + candidates = [n for n in after_suppress if n in weights] + if len(candidates) > 1: + # Gewinner nach höchstem Gewicht + best = max(candidates, key=lambda n: weights[n]) + # alle anderen Kandidaten dieser Gruppe fallen raus + drop_from_groups.update(n for n in candidates if n != best) + + final_over_names = [n for n in after_suppress if n not in drop_from_groups] + + # zurückschreiben + key_to_overlays[over_key] = (item, final_over_names) return key_to_overlays, properties def get_overlay_items(self, label="Overlay", libtype=None, ignore=None): - items = self.library.search(label=label, libtype=libtype) + # items = self.library.search(label=label, libtype=libtype) + if libtype: + emby_items = self.library.EmbyServer.get_items(include_item_types=[libtype], params={"ParentId": self.library.EmbyServer.library_id, "Recursive": "True"}) + else: + emby_items = self.library.EmbyServer.get_items(params={"ParentId": self.library.EmbyServer.library_id, "Recursive": "True"},include_item_types =["Series,Movie,Season,Episode"]) + all_emby_ids = [item.get("Id") for item in emby_items] + all_emby_ids = all_emby_ids if not ignore else [o for o in all_emby_ids if o not in ignore] + # todo: WIP + return all_emby_ids return items if not ignore else [o for o in items if o.ratingKey not in ignore] def remove_overlay(self, item, item_title, label, locations): + #todo: delete overlay png from Emby plugin folder try: - poster, _, _, _ = self.library.find_item_assets(item) + poster, _, _, _, _ = self.library.find_item_assets(item) except Failed: poster = None is_url = False @@ -695,17 +728,17 @@ def remove_overlay(self, item, item_title, label, locations): poster_location = poster.location elif any([os.path.exists(loc) for loc in locations]): poster_location = next((loc for loc in locations if os.path.exists(loc))) - if not poster_location: - is_url = True - try: - poster_location = self.library.item_posters(item) - except Failed: - pass + # if not poster_location: + # is_url = True + # try: + # poster_location = self.library.item_posters(item) + # except Failed: + # pass if poster_location: - self.library.upload_poster(item, poster_location, url=is_url) + # self.library.upload_poster(item, poster_location, url=is_url) self.library.edit_tags("label", item, remove_tags=[label], do_print=False) for loc in locations: if os.path.exists(loc): os.remove(loc) else: - logger.error(f"No Poster found to restore for {item_title}") + logger.error(f"No Overlay found to remove for {item_title}") diff --git a/modules/plex.py b/modules/plex.py index eb686e009..87ed967db 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1,22 +1,32 @@ -import os, plexapi, re, time +import os +import plexapi +import re +import time from datetime import datetime, timedelta -from modules import builder, util -from modules.library import Library -from modules.poster import ImageData -from modules.request import parse_qs, quote_plus, urlparse -from modules.util import Failed +# from importlib.metadata import pass_none +from urllib.parse import unquote +from xml.etree.ElementTree import ParseError + +import requests from PIL import Image from plexapi import utils from plexapi.audio import Artist, Track, Album -from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.collection import Collection +from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.library import Role, FilterChoice from plexapi.playlist import Playlist from plexapi.server import PlexServer from plexapi.video import Movie, Show, Season, Episode from requests.exceptions import ConnectionError, ConnectTimeout from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type -from xml.etree.ElementTree import ParseError + +from modules import builder, util +from modules.emby_server import EmbyServer, FilterChoiceEmby +from modules.library import Library +from modules.logs import WARNING +from modules.poster import ImageData +from modules.request import parse_qs, quote_plus, urlparse +from modules.util import Failed logger = util.logger @@ -48,6 +58,7 @@ "episode_last_played": "episode.lastViewedAt", "unplayed": "unwatched", "episode_unplayed": "episode.unwatched", + "dovi": "dovi", "subtitle_language": "subtitleLanguage", "audio_language": "audioLanguage", "progress": "inProgress", @@ -149,6 +160,7 @@ "genre": "genres", "label": "labels", "producer": "producers", + "composer": "composers", "release": "originallyAvailableAt", "originally_available": "originallyAvailableAt", "added": "addedAt", @@ -193,6 +205,7 @@ "studios": "studio", "networks": "network", "producers": "producer", + "composers": "composer", "writers": "writer", "years": "year", "show_year": "year", "show_years": "year", "show_title": "title", "filter": "filters", @@ -257,15 +270,15 @@ and_searches = [ "title.and", "studio.and", "actor.and", "audio_language.and", "collection.and", "content_rating.and", "country.and", "director.and", "genre.and", "label.and", - "network.and", "producer.and", "subtitle_language.and", "writer.and" + "network.and", "producer.and", "composer.and", "subtitle_language.and", "writer.and" ] or_searches = [ "title", "studio", "actor", "audio_language", "collection", "content_rating", - "country", "director", "genre", "label", "network", "producer", "subtitle_language", + "country", "director", "genre", "label", "network", "producer", "composer", "subtitle_language", "writer", "decade", "resolution", "year", "episode_title", "episode_year" ] movie_only_searches = [ - "director", "director.not", "producer", "producer.not", "writer", "writer.not", + "director", "director.not", "producer", "producer.not", "composer", "composer.not", "writer", "writer.not", "decade", "duplicate", "unplayed", "progress", "duration.gt", "duration.gte", "duration.lt", "duration.lte" "edition", "edition.not", "edition.is", "edition.isnot", "edition.begins", "edition.ends" @@ -291,10 +304,10 @@ string_attributes = ["title", "studio", "edition", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends"] boolean_attributes = [ - "hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", "unplayed_episodes", "episode_unplayed", + "dovi", "hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", "unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched", "show_unmatched", "artist_unmatched", "album_unmatched", "track_trash" ] -tmdb_attributes = ["actor", "director", "producer", "writer"] +tmdb_attributes = ["actor", "director", "producer", "composer", "writer"] date_attributes = [ "added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played", "artist_added", "artist_last_played", "album_last_played", @@ -312,7 +325,7 @@ search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"} tag_attributes = [ "actor", "episode_actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "season_label", "episode_label", "network", - "producer", "resolution", "studio", "subtitle_language", "writer", "season_collection", "episode_collection", "edition", + "producer", "composer", "resolution", "studio", "subtitle_language", "writer", "season_collection", "episode_collection", "edition", "artist_genre", "artist_collection", "artist_country", "artist_mood", "artist_label", "artist_style", "album_genre", "album_mood", "album_style", "album_format", "album_type", "album_collection", "album_source", "album_label", "track_mood", "track_source", "track_label" ] @@ -442,14 +455,17 @@ "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", } -MAX_IMAGE_SIZE = 10480000 # a little less than 10MB class Plex(Library): def __init__(self, config, params): super().__init__(config, params) + + self.filter_items_cache = {} self.plex = params["plex"] - self.url = self.plex["url"] - self.session = self.config.Requests.session + self.url = self.plex["url"] # todo also used in webhooks for image retrieval + self.emby = params["emby"] + self.emby_server_url = self.emby["url"] + self.session = self.config.Requests.session # init? if self.plex["verify_ssl"] is False and self.config.Requests.global_ssl is True: logger.debug("Overriding verify_ssl to False for Plex connection") self.session = self.config.Requests.create_session(verify_ssl=False) @@ -457,11 +473,16 @@ def __init__(self, config, params): logger.debug("Overriding verify_ssl to True for Plex connection") self.session = self.config.Requests.create_session() self.token = self.plex["token"] + self.emby_api_key = self.emby["api_key"] + self.emby_user_id = self.emby["user_id"] + self.overlay_destination_folder = self.emby["overlay_destination_folder"] self.timeout = self.plex["timeout"] logger.secret(self.url) logger.secret(self.token) + self.EmbyServer = None try: self.PlexServer = PlexServer(baseurl=self.url, token=self.token, session=self.session, timeout=self.timeout) + self.EmbyServer = EmbyServer(self.emby_server_url, self.emby_user_id, self.emby_api_key,config, params["name"]) plexapi.server.TIMEOUT = self.timeout os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(self.timeout) logger.info(f"Connected to server {self.PlexServer.friendlyName} version {self.PlexServer.version}") @@ -508,34 +529,98 @@ def __init__(self, config, params): library_names = [] for s in self.PlexServer.library.sections(): library_names.append(s.title) - if s.title == params["name"]: - self.Plex = s + #emby, just use first lib as dummy + # if s.title == params["name"]: + self.Plex = s + break + + self.Emby = None + + emby_library_names = [] + # print(params) + self.lib_type = None + for s in self.EmbyServer.get_libraries(): + # print(s) + emby_library_names.append(s["Name"]) + if s["CollectionType"] == 'tvshows': + self.lib_type = "show" + elif s["CollectionType"] == 'movies': + self.lib_type = "movie" + if s["Name"] == params["name"]: + self.Emby = s + self.EmbyServer.library_id= self.Emby.get('Id') + print(s) break - if not self.Plex: - raise Failed(f"Plex Error: Plex Library '{params['name']}' not found. Options: {library_names}") - if self.Plex.type not in library_types: - raise Failed(f"Plex Error: Plex Library must be a Movies, TV Shows, or Music library") - if not self.Plex.allowSync: - raise Failed("Plex Error: Plex Token is read only. Please get a new token") - - self.type = self.Plex.type.capitalize() + # print(emby_library_names) + if not self.Emby: + raise Failed(f"Emby Error: Emby Library '{params['name']}' not found. Options: {emby_library_names}") + # -------------- + + # if not self.Plex: + # raise Failed(f"Plex Error: Plex Library '{params['name']}' not found. Options: {library_names}") + # if self.Plex.type not in library_types: + # raise Failed(f"Plex Error: Plex Library must be a Movies, TV Shows, or Music library") + # if not self.Plex.allowSync: + # raise Failed("Plex Error: Plex Token is read only. Please get a new token") + + # self.type = self.Plex.type.capitalize() + self.type = self.Emby.get("CollectionType", "") + # Entferne das 's', wenn self.type 'movies' oder 'shows' ist + + # Now, find out the library type + collection_type = self.Emby.get("CollectionType", "").lower() + if collection_type == "movies": + self.emby_type = "Movie" + elif collection_type == "tvshows": + self.emby_type = "Show" + elif collection_type == "music": + self.emby_type = "Artist" + else: + self.emby_type = "Other" + self.type= self.emby_type + # print(f"Collection type is: '{collection_type}'") + # coll = Collection() + if self.emby_type.lower() not in library_types: + raise Failed(f"Emby Error: Emby Library must be a Movies, TV Shows, or Music library") + + + + # print(f"EMBY Library type: {self.type}") + # print(self.type) self.plex_pass = self.PlexServer.myPlexSubscription self._users = [] + self.emby_users = [] self._all_items = [] + self._emby_all_items = [] + self._emby_all_items_native = [] self._account = None self.agent = self.Plex.agent self.scanner = self.Plex.scanner source_setting = next((s for s in self.Plex.settings() if s.id in ["ratingsSource"]), None) + # Todo + # print(f"Checkie: {source_setting}") + # Checkie: <Setting:ratingsSource:rottentomatoes> + # Checkie: <Setting:ratingsSource:imdb> + # Checkie: <Setting:ratingsSource:themoviedb> self.ratings_source = source_setting.enumValues[source_setting.value] if source_setting else "N/A" - self.is_movie = self.type == "Movie" - self.is_show = self.type == "Show" - self.is_music = self.type == "Artist" - self.is_other = self.agent == "com.plexapp.agents.none" + + # self.is_movie = self.type == "Movie" + # self.is_show = self.type == "Show" + # self.is_music = self.type == "Artist" + # self.is_other = self.agent == "com.plexapp.agents.none" + + self.is_movie = self.emby_type == "Movie" + self.is_show = self.emby_type == "Show" + self.is_music = self.emby_type == "Artist" + self.is_other = self.emby_type == "Other" + + # todo: needed for Emby? if self.is_other and self.type == "Movie": self.type = "Video" if not self.is_music and self.update_blank_track_titles: self.update_blank_track_titles = False logger.error(f"update_blank_track_titles library operation only works with music libraries") + logger.info(f"Connected to library {params['name']}") logger.info(f"Type: {self.type}") logger.info(f"Agent: {self.agent}") @@ -544,15 +629,23 @@ def __init__(self, config, params): def notify(self, text, collection=None, critical=True): self.config.notify(text, server=self.PlexServer.friendlyName, library=self.name, collection=collection, critical=critical) + # pass def notify_delete(self, message): self.config.notify_delete(message, server=self.PlexServer.friendlyName, library=self.name) + # pass def set_server_preroll(self, preroll): self.PlexServer.settings.get('cinemaTrailersPrerollID').set(preroll) self.PlexServer.settings.save() + # pass + # not needed, refer to fetchItems def get_all_collections(self, label=None): + + lib_id = self.Emby.get("Id") + return self.EmbyServer.get_boxsets_from_library(library_id = lib_id, label=label, native=True) + args = "?type=18" if label: label_id = next((c.key for c in self.get_tags("label") if c.title == label), None) # noqa @@ -560,11 +653,33 @@ def get_all_collections(self, label=None): args = f"{args}&label={label_id}" else: return [] + # todo add label? + + + return self.fetchItems(args) @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def search(self, title=None, sort=None, maxresults=None, libtype=None, **kwargs): - return self.Plex.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs) + # print(title) + if libtype == "collection": + lib_id = self.Emby.get("Id") + + return self.EmbyServer.get_boxsets_from_library(title, library_id=lib_id) + + # return self.EmbyServer.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs) + pass + else: + pass + # print(f"EMBY style: {self.EmbyServer.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs)}") + # print(f"plex_search:{title} - {sort} - {maxresults} - {libtype} - {kwargs}") + # print(self.EmbyServer.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs)) + # print(self.Plex.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs)) + + # plex_search: IMDb Lowest Rated - None - None - collection - {} + # [ < Collection: 204155:IMDb - Lowest - Rated >] + + return self.EmbyServer.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs) @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def exact_search(self, title, libtype=None, year=None): @@ -573,29 +688,546 @@ def exact_search(self, title, libtype=None, year=None): terms["year"] = year return self.Plex.search(libtype=libtype, **terms) + def get_native_emby_item(self, emby_item_id): + return self.EmbyServer.get_item(emby_item_id) + pass + + def fetch_item(self, item): if isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)): - return self.reload(item) + return item + # return self.reload(item) key = int(item) if key in self.cached_items: - return self.reload(self.cached_items[key][0]) + # emby no reload + return self.cached_items[key][0] + # return self.reload(self.cached_items[key][0]) try: current = self.fetchItem(key) if isinstance(current, (Movie, Show, Season, Episode, Artist, Album, Track)): - return self.reload(current) + return current + # return self.reload(current) except (BadRequest, NotFound) as e: logger.trace(e) raise Failed(f"Plex Error: Item {item} not found") @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def fetchItem(self, data): + # print(f"fetchItem(self, data): {data}") + # print(f"fetchItem: {self.PlexServer.fetchItem(data)}") + # print("-------------") + item = self.EmbyServer.get_item(data) + return self.EmbyServer.convert_emby_to_plex([item])[0] return self.PlexServer.fetchItem(data) - @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + # @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), + # retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + def fetchItems(self, uri_args): - return self.Plex.fetchItems(f"/library/sections/{self.Plex.key}/all{'' if uri_args is None else uri_args}") + """ + Fetch items from Plex or Emby based on the provided URI arguments. + Supports decade-based filtering for Emby and correctly handles episodes. + """ + is_show= False + additional_person_search = [] + # Parse the URI arguments + plus_replace = str(uri_args).replace('+', '%2B') + + args = parse_qs(plus_replace.lstrip('?')) + + # Default-Datenstruktur für mehrere Instanzen + from collections import defaultdict + param_values = defaultdict(list) + for key, values in args.items(): + for value in values: + param_values[unquote(key)].append(unquote(value)) + + # Initialize Emby API query parameters + emby_query_params = {} + unknown_params = {} + emby_query_params["Recursive"] = "true" + if "or" in args: + pass + + # Initialize 'Years' list and item types + years_list = [] + item_types = set() + + # Process 'type' parameter + type_values = args.get('type', []) + for type_value in type_values: + if type_value == '1': + item_types.add('Movie') + elif type_value == '2': + item_types.add('Series') + elif type_value == '18': + item_types.add('BoxSet') # Assuming 'BoxSet' for collections + else: + raise Failed(f"Unknown type value: {type_value} {uri_args}") + + # Process each parameter + for key, values in param_values.items(): + for value in values: + key_decoded = unquote(key) + value_decoded = unquote(value) + + # Detect 'episode.' or 'show.' fields for item types + # if key_decoded.startswith('episode.'): + # item_types = {"Episode"} + + # Handle parameters with comparison operators in the key + match = re.match(r'([\w\.]+)([<>]{1,2}=?)(.*)', key_decoded) + if match: + field, operator, _ = match.groups() + field = field.strip() + operator = operator.strip() + operand = value_decoded.strip() + + if field in ["rating","show.rating"]: + emby_query_params["Fields"]= "CommunityRating,CriticRating,CustomRating" + if operator in ['>', '>=']: + emby_query_params['MinCriticRating'] = int(float(operand) * 10) + elif operator in ['<', '<=','<<']: + emby_query_params['MaxCriticRating'] = int(float(operand) * 10) + else: + raise Failed(f"Unknown operator {operator} for {field}") + elif field in ["audienceRating", "show.audienceRating"]: + emby_query_params["Fields"]= "CommunityRating,CriticRating,CustomRating" + if operator in ['>', '>=']: + emby_query_params['MinCommunityRating'] = operand + elif operator in ['<', '<=','<<']: + emby_query_params['MaxCommunityRating'] = operand + else: + raise Failed(f"Unknown operator {operator} for {field}") + elif field in ["userRating", "show.userRating"]: + emby_query_params["Fields"]= "CommunityRating,CriticRating,CustomRating" + if operator in ['>', '>=']: + emby_query_params['MinCustomRating'] = operand + elif operator in ['<', '<=','<<']: + emby_query_params['MaxCustomRating'] = operand + else: + raise Failed(f"Unknown operator {operator} for {field}") + elif field.endswith('originallyAvailableAt'): + if field.startswith("episode"): # look for episodes recently aired to get to the show + is_show = True + item_types.add("Episode") + # item_types = {"Series"} + + date_value = self.parse_relative_date(operand) + if date_value: + if operator in ['>>', '>=', '>>=']: + emby_query_params['MinPremiereDate'] = date_value.isoformat() + elif operator in ['<<', '<=', '<<=']: + emby_query_params['MaxPremiereDate'] = date_value.isoformat() + else: + unknown_params['operator'] = operator + else: + print(f"Unable to parse date value: {operand}") + else: + unknown_params[key_decoded] = value_decoded + else: + hdr_ids = list(set( + # self.EmbyServer.media_by_resolution.get("dv", []) + + self.EmbyServer.media_by_resolution.get("dvhdr", []) + + self.EmbyServer.media_by_resolution.get("dvhdrplus", []) + + self.EmbyServer.media_by_resolution.get("hdr", []) + + self.EmbyServer.media_by_resolution.get("plus", []) + )) + + # Process regular parameters + if key_decoded in ['type', 'and']: + pass # Already handled above + elif key_decoded in ['studio=', 'studio', 'show.studio', 'show.studio=']: # todo add newtwork here for later + # Handle multiple studios + # if 'Studios' not in emby_query_params: + # emby_query_params['Studios'] = [] + # is this working correctly? + if "Studios" in emby_query_params: + emby_query_params["Studios"].append(value_decoded) + else: + emby_query_params['Studios']= [value_decoded] + elif key_decoded in ['show.network']: # todo add newtwork here for later + # TODO: Use Emby Studio for Studios and Networks. Too much work with auto updates. + if "Studios" in emby_query_params: + # emby_query_params["Studios"].append(f"📡 {value_decoded}") + emby_query_params["Studios"].append(f"{value_decoded}") + else: + # emby_query_params['Studios']= [f"📡 {value_decoded}"] + emby_query_params['Studios']= [f"{value_decoded}"] + elif key_decoded == 'country': + + if 'Ids' not in emby_query_params: + emby_query_params['Ids'] = [] + + for it, val in self.EmbyServer.production_search.items(): + if value in val: + emby_query_params['Ids'].append(it) + elif f"{self.name} {value_decoded}" in val: + emby_query_params['Ids'].append(it) + + # emby_query_params['Ids'].append(encode_tags_to_uri(emby_item_ids)) + + + # e_items = [] + # for id in emby_item_ids: + # e_items.append(self.EmbyServer.get_item(id)) + # + # # mn = self.EmbyServer.get_items({'Ids': emby}) + # mn = self.EmbyServer.convert_emby_to_plex(e_items) + # # todo: add sort order etc. + # return mn + + elif key_decoded == 'genre': + if "Genres" not in emby_query_params: + emby_query_params['Genres'] = [value_decoded] + emby_query_params["Recursive"]= "true" + + else: + emby_query_params['Genres'].append(value_decoded) + elif key_decoded == 'limit': + emby_query_params['Limit'] = value_decoded + elif key_decoded == 'show.contentRating' or key_decoded == 'contentRating': + if "OfficialRatings" not in emby_query_params: + emby_query_params['OfficialRatings'] = [value_decoded] + else: + emby_query_params['OfficialRatings'].append(value_decoded) + elif key_decoded in ['label', 'show.label']: + # Handle multiple labels + icon = '📺' if self.type == 'Show' else '🎥' + name = self.name + composed_name = f'{icon} {name} ' + if 'Tags' not in emby_query_params: + emby_query_params['Tags'] = [] + emby_query_params['Tags'].append(f'{composed_name}{value_decoded}') + emby_query_params['Tags'].append(f'{value_decoded}') + elif key_decoded in ['actor', 'director', 'writer', 'producer', 'composer', 'show.actor']: + # Handle multiple persons + # item_types.add("Person") + if 'PersonIds' not in emby_query_params: + emby_query_params['PersonIds'] = [] + if 'PersonTypes' not in emby_query_params: + emby_query_params['PersonTypes'] = [] + if key_decoded.startswith('show.'): + key_decoded = key_decoded.split('.')[1] + emby_query_params['PersonIds'].append(value_decoded) + emby_query_params['PersonTypes'].append(key_decoded) + additional_person_search.append(value_decoded) # Emby item id + elif key_decoded == 'sort': + sort_parts = value_decoded.split(':') + sort_field, sort_order = (sort_parts[0], sort_parts[1]) if len(sort_parts) == 2 else ( + value_decoded, 'asc') + + if sort_field == 'audienceRating': + emby_query_params['SortBy'] = 'CommunityRating' + elif sort_field in ['title', 'titleSort']: + emby_query_params['SortBy'] = 'Name' + elif sort_field == 'originallyAvailableAt': + emby_query_params['SortBy'] = 'PremiereDate' + elif sort_field == 'rating': + emby_query_params['SortBy'] = 'CriticRating' + elif sort_field == 'random': + emby_query_params['SortBy'] = 'Random' + elif sort_field in ['addedAt', 'episode.addedAt']: + emby_query_params['SortBy'] = 'DateCreated' + else: + unknown_params['sort_field'] = sort_field + + emby_query_params['SortOrder'] = 'Descending' if sort_order.lower() == 'desc' else 'Ascending' + elif key_decoded == 'decade': + decade = int(value_decoded) + years_list.extend(str(year) for year in range(decade, decade + 10)) + elif key_decoded in ('year', 'show.year', 'episode.year'): + if value_decoded.isdigit(): + years_list.append(value_decoded) + elif key_decoded in ['resolution']: + #add the missing p in search + index_key = value_decoded + if index_key not in ["4k","HD","4K"]: + index_key += 'p' + elif index_key == "HD": + index_key = "720p" + try: + all_ids = self.EmbyServer.media_by_resolution[index_key] + except: + raise Failed(f"Decoded value {value_decoded} not in search!") + # "MediaStreams": [ "VideoRange": "HDR 10"], + # Builder: plex_search: (1, "Plex Movie Search\nSort By: ['title.asc']\nFilter:\n Match all of the following:\n Critic Rating is greater than or equal 0.0\n Critic Rating is less than 6.0", '?type=1&sort=titleSort&rating%3E=0.0&and=1&rating%3C%3C=6.0') | + # todo rating search broken; critic 0-100 vs 0-10.0 ? + search_ids = [] + + if "hdr" in args and args["hdr"] == ['1']: + for id in all_ids: + if id in hdr_ids: + search_ids.append(id) + else: + search_ids = all_ids + if len(search_ids) == 0: + return [] + id_string = ",".join(search_ids) + items = [item for item in self._emby_all_items if str(item.ratingKey) in search_ids] + # all_emby_items = self.EmbyServer.get_items(params={'Ids': id_string}) + # plex_items = self.EmbyServer.convert_emby_to_plex(items) + return items + # match value_decoded: + # case '4k': + # emby_query_params['Is4K'] = 'true' + # break + # case '1080p': + # emby_query_params['IsHD'] = 'true' + # break + # case _: + # Failed(f"Unknown parameter: {unknown_params} {uri_args}") + # MinWidth=1200 + # MaxWidth=1200 + # MinHeight=1200 + # MaxHeight=1200 + #todo: handle resolutions from filter + elif key_decoded == "hdr" in args and value_decoded == "1": + if len(hdr_ids) == 0: + return [] + id_string = ",".join(hdr_ids) + all_emby_items = self.EmbyServer.get_items(params={'Ids': id_string}) + plex_items = self.EmbyServer.convert_emby_to_plex(all_emby_items) + return plex_items + + else: + if key_decoded not in ["pop", "push", "or"]: + unknown_params[key_decoded] = value_decoded + + # resolution: + # {'resolution': '4k'} + # {'resolution': '4k', 'hdr': '1'} + # {'resolution': '1080'} + # {'resolution': 'HD'} + # {'resolution': '576'} + # {'resolution': '480'} + + # retrieves all media + # 📺 Serien CBS + # 📺 Serien Max + # if '📺 Serien Sky' in emby_query_params.get('Tags', []): + # pass + + # Combine multi-value parameters + if 'Ids' in emby_query_params: + emby_query_params['Ids'] = ','.join(emby_query_params['Ids']) + if 'Studios' in emby_query_params: + emby_query_params['Studios'] = ','.join(emby_query_params['Studios']) + if 'Tags' in emby_query_params: + emby_query_params['Tags'] = '|'.join(emby_query_params['Tags']) + if 'PersonIds' in emby_query_params: + emby_query_params['PersonIds'] = ','.join(emby_query_params['PersonIds']) + if 'PersonTypes' in emby_query_params: + emby_query_params['PersonTypes'] = ','.join(set(emby_query_params['PersonTypes'])) + if 'OfficialRatings' in emby_query_params: + emby_query_params['OfficialRatings'] = '|'.join(set(emby_query_params['OfficialRatings'])) + + + # Set 'Years' parameter if years_list is not empty + if years_list: + emby_query_params['Years'] = ','.join(years_list) + + # Set IncludeItemTypes in query params + if item_types: + emby_query_params['IncludeItemTypes'] = ','.join(item_types) + + emby_query_params['ParentId'] = self.Emby.get("Id") + + if unknown_params: + logger.error(f"Emby BETA: unknown parameters: {unknown_params}") + # | 1 | Unknown parameter: {'duplicate': '1'} ?type=1&sort=titleSort&duplicate=1 + raise Failed(f"Unknown parameter: {unknown_params} {uri_args}") + + # Query Emby API to get items matching criteria + # if re.search("Miramax",uri_args): + # pass + + + + items = self.EmbyServer.get_items(emby_query_params) + all_shows = None + if is_show: + all_shows= [] + # only the show is requestes + for item in items: + my_id = item.get("SeriesId") + my_series = self.EmbyServer.get_item(my_id) + all_shows.append(my_series) + + if all_shows: + my_output= self.EmbyServer.convert_emby_to_plex(all_shows) + else: + my_output= self.EmbyServer.convert_emby_to_plex(items) + # Convert Emby items to Plex format + # Used for Emby to retrieve the person and add to collection + if additional_person_search: + people = [] + for add_p in additional_person_search: + if not add_p.isdigit(): + continue + person = self.EmbyServer.get_item(add_p) + people.append(person) + plex_person = self.EmbyServer.convert_emby_to_plex(people, False) + if plex_person: + my_output.extend(plex_person) + else: + logger.warning(f"Additional person search was requested, result unclear: {additional_person_search} => {plex_person}") + return my_output + + def parse_relative_date(self, relative_date_str): + """ + Parses a relative date string like '-90d' and returns a datetime object. + """ + match = re.match(r'(-?\d+)([dwmy])', relative_date_str) + if not match: + return None + + value, unit = match.groups() + value = int(value) + now = datetime.now() + + if unit == 'd': + return now + timedelta(days=value) + elif unit == 'w': + return now + timedelta(weeks=value) + elif unit == 'm': + # Approximate a month as 30 days + return now + timedelta(days=value * 30) + elif unit == 'y': + # Approximate a year as 365 days + return now + timedelta(days=value * 365) + else: + return None + + + # @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + # def fetchItems(self, uri_args): + # # print(uri_args) + # # todo + # if uri_args != "?type=18": # 18 == collections + # print(f"fetchItems: {uri_args}") + # # fetchItems: ?type=18 + # # [<Collection:204179:🎖Veteran's-Day-Movie>, <Collection:204180:🦃-Thanksgiving-Movie>] + # print(self.Plex.fetchItems(f"/library/sections/{self.Plex.key}/all{'' if uri_args is None else uri_args}")) + # print("-----------------------") + # else: + # return self.EmbyServer.get_boxsets_from_library() + # # print("jhjhbjbjb") + # return self.Plex.fetchItems(f"/library/sections/{self.Plex.key}/all{'' if uri_args is None else uri_args}") + + def get_provider_ids(self, item): + return self.EmbyServer.get_provider_ids(item) + + + def emby_get_all(self, builder_level=None, load=False, native = False): + """ + Retrieves all items from the library, optionally filtering by builder_level. + + Parameters: + builder_level (str): The level to build (e.g., 'movie', 'show', 'artist'). + load (bool): Whether to reload the items. + + Returns: + list: A list of all items. + """ + # print(builder_level) + # if not native and load and builder_level in [None, "show", "artist", "movie"]: + # self._emby_all_items = [] + # self._emby_all_items_native = [] + if not native and self._emby_all_items and builder_level in [None, "show", "artist", "movie"]: + return self._emby_all_items + if native and self._emby_all_items_native and builder_level in [None, "show", "artist", "movie"]: + return self._emby_all_items_native + + # builder_type = builder_level.lower() if builder_level else self.Plex.TYPE + + builder_type = builder_level.lower() if builder_level else self.type.lower() + if not builder_level: + builder_level = self.type.lower() + + logger.info(f"Loading All {builder_level.capitalize()}s from Library: {self.Emby.get('Name')}") + + items = [] + start_index = 0 + limit = 250 + total_record_count = 1 + include_item_types = [] + # print(builder_type) + # Bestimmung der Typen für die Abfrage + # ToDo: Add more builder_types + if builder_type == "movie": + include_item_types = ["Movie"] + elif builder_type == "show": + include_item_types = ["Series"] + elif builder_type == "season": + include_item_types = ["Season"] + elif builder_type == "artist": + include_item_types = ["MusicArtist"] + else: + logger.warning(f"builder type not supported by 'emby_get_all' - {builder_type}") + include_item_types = ["Movie", "Series", "MusicArtist"] + items_data =[] + while start_index < total_record_count: + # Abfrage der Hauptdaten + params = { + "Recursive": "true", + "IncludeItemTypes": ",".join(include_item_types), + "StartIndex": start_index, + "Limit": limit, + "ParentId": self.Emby.get("Id"), + "Fields": "Budget,Chapters,DateCreated,Genres,HomePageUrl,IndexOptions,MediaStreams,Overview,ParentId,Path,People,ProviderIds,PrimaryImageAspectRatio,Revenue,SortName,Studios,Taglines,CriticRating,CustomRating,CommunityRating", + } + + endpoint = f"{self.emby_server_url}/emby/Users/{self.emby_user_id}/Items" + response = requests.get(endpoint, headers=self.EmbyServer.headers, params=params) + # response = self.session.get(endpoint, headers=self.EmbyServer.headers, params=params) + response.raise_for_status() + data = response.json() + + + # print(data) + + # Gesamtdatensätze und Fortschritt verfolgen + items_data += data.get("Items", []) + total_record_count = data.get("TotalRecordCount", 0) + start_index += limit + logger.ghost( + f"Loaded: {start_index if start_index < total_record_count else total_record_count}/{total_record_count}") + + self.EmbyServer.cache_filenames(items_data) + + logger.info(f"Loaded {len(items_data)} {builder_level.capitalize()}s from Emby") + self._emby_all_items_native = items_data + if native: + # for item in items_data: + # for people in item.get("People", []): + # params = { + # "Recursive": "true", + # "IncludeItemTypes": "People", + # "Ids": people.get('Id'), + # "Fields": "ProviderIds", + # } + # response = self.session.get(endpoint, headers=self.emby_headers, params=params) + # prov_ids = response.json().get('Items', [])[0].get('ProviderIds') + # if prov_ids: + # tmdb_id = prov_ids.get('Tmdb', None) + # if tmdb_id: + # people['tmdb_id'] = tmdb_id + return items_data + plex_items= self.EmbyServer.convert_emby_to_plex(items_data) + # if builder_level in [None, "show", "artist", "movie"]: + self._emby_all_items = plex_items + return plex_items + + def get_all_native(self, builder_level=None, load = False): + return self.emby_get_all(builder_level, load, native=True) + def get_all(self, builder_level=None, load=False): + # print(builder_level) + + return self.emby_get_all(builder_level, load) + if load and builder_level in [None, "show", "artist", "movie"]: self._all_items = [] if self._all_items and builder_level in [None, "show", "artist", "movie"]: @@ -626,9 +1258,16 @@ def get_all(self, builder_level=None, load=False): logger.info(f"Loaded {total_size} {builder_level.capitalize()}s") if builder_level in [None, "show", "artist", "movie"]: self._all_items = results + + # print("-------------") + # print(self.emby_get_all(builder_level, load)) + # print("-------------") + # print(results) + return results def upload_theme(self, collection, url=None, filepath=None): + # todo should be file based key = f"/library/metadata/{collection.ratingKey}/themes" if url: self.PlexServer.query(f"{key}?url={quote_plus(url)}", method=self.PlexServer._session.post) @@ -649,9 +1288,23 @@ def moveItem(self, obj, item, after): @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def query(self, method): + # print("query!!!!!!") return method() def delete(self, obj): + if isinstance(obj, Collection): + # print(f"EMBY DELETE: {obj}") + self.EmbyServer.delete_collection(obj) + return + elif isinstance(obj, list) and len(obj) == 0: + return + else: + print(f"Failed to delete object {obj}") + logger.stacktrace() + return + raise Failed(f"Plex Error: Failed to delete {obj.title}") + + # return try: return self.query(obj.delete) except Exception: @@ -684,7 +1337,21 @@ def collection_order_query(self, collection, data): @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def item_labels(self, item): try: - return item.labels + # Prüfe, ob das Plex/Emby-Objekt ein `ratingKey` hat + rating_key = getattr(item, "ratingKey", None) + if not rating_key: + raise Failed(f"Item: {getattr(item, 'title', 'Unknown')} does not have a valid ratingKey.") + + # Hole die Labels/Tags vom Emby-Server + tags = self.EmbyServer.get_emby_item_tags(item, self.Emby.get("Id")) + + # Wrappe jeden Tag in ein Objekt mit Attribut .tag + class Label: + def __init__(self, tag): + self.tag = tag + + return [Label(t) for t in tags] + except BadRequest: raise Failed(f"Item: {item.title} Labels failed to load") @@ -747,6 +1414,8 @@ def item_posters(self, item, providers=None): return image_url def item_reload(self, item): + return item + item.reload(checkFiles=False, includeAllConcerts=False, includeBandwidths=False, includeChapters=False, includeChildren=False, includeConcerts=False, includeExternalMedia=False, includeExtras=False, includeFields=False, includeGeolocation=False, includeLoudnessRamps=False, includeMarkers=False, @@ -768,15 +1437,11 @@ def load_list_from_cache(self, rating_keys): item_list.append(item) return item_list - def validate_image_size(self, image): - if image.compare < MAX_IMAGE_SIZE: - return True - else: - logger.error(f"Image too large: {image.location}, bytes {image.compare}, MAX {MAX_IMAGE_SIZE}") - return False @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def reload(self, item, force=False): + return item + is_full = False if not force and item.ratingKey in self.cached_items: item, is_full = self.cached_items[item.ratingKey] @@ -808,17 +1473,21 @@ def _upload_image(self, item, image): now = datetime.now() self.config.tpdb_timer = now if image.is_poster and image.is_url: - item.uploadPoster(url=image.location) + upload_success = self.upload_poster(item, image.location, url=True) elif image.is_poster: upload_success = self.validate_image_size(image) if upload_success: - item.uploadPoster(filepath=image.location) - elif image.is_url: + self.upload_poster(item, image.location) + elif image.is_background and image.is_url: item.uploadArt(url=image.location) - else: + elif image.is_background: upload_success = self.validate_image_size(image) if upload_success: item.uploadArt(filepath=image.location) + elif image.is_url: + item.uploadLogo(url=image.location) + else: + item.uploadLogo(filepath=image.location) self.reload(item, force=True) return upload_success except BadRequest as e: @@ -826,27 +1495,66 @@ def _upload_image(self, item, image): raise Failed(e) @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + def upload_poster_overlay(self, item, image_temp_path, url=False): + # Not actually uploading anything to Emby, just saving the overlay png + file_extension = image_temp_path.split('.')[-1] + file_name = f"{item.ratingKey}.{file_extension}" + # todo: config path + + export_file = os.path.join(self.overlay_destination_folder, file_name) + + try: + # Erstellen des Zielverzeichnisses, falls es nicht existiert + destination_dir = os.path.dirname(self.overlay_destination_folder) + if not os.path.exists(destination_dir): + os.makedirs(destination_dir) + + # Kopiere die Datei zum Ziel + import shutil + shutil.copy2(image_temp_path, export_file) + # print(f"Datei erfolgreich kopiert: {image_temp_path} -> {export_file}") + except Exception as e: + # print(f"Fehler beim Kopieren der Datei: {e}") + raise + def upload_poster(self, item, image, url=False): if url: - item.uploadPoster(url=image) + return self.EmbyServer.set_image_smart(item.ratingKey, image) + # item.uploadArt(url=image) else: - item.uploadPoster(filepath=image) + return self.EmbyServer.set_image_smart(item.ratingKey, image) + # if url: + # item.uploadPoster(url=image) + # else: + # item.uploadPoster(filepath=image) @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) def upload_background(self, item, image, url=False): if url: - item.uploadArt(url=image) + self.EmbyServer.set_image_smart(item.ratingKey, url, image_type="Backdrop") + # item.uploadArt(url=image) + else: + self.EmbyServer.set_image_smart(item.ratingKey, image, image_type="Backdrop") + # item.uploadArt(filepath=image) + + @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + def upload_logo(self, item, image, url=False): + if url: + self.EmbyServer.set_image_smart(item.ratingKey, url, image_type="ClearLogo") else: - item.uploadArt(filepath=image) + self.EmbyServer.set_image_smart(item.ratingKey, image, image_type="ClearLogo") @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type(Failed)) def get_actor_id(self, name): + + return self.EmbyServer.get_actor_id(name) + results = self.Plex.hubSearch(name) for result in results: if isinstance(result, Role) and result.librarySectionID == self.Plex.key and result.tag == name: return result.id - def get_search_choices(self, search_name, title=True, name_pairs=False): + def get_search_choices(self, search_name, title=True, name_pairs=False, person_list = None, tmdb_person_id = None): final_search = search_translation[search_name] if search_name in search_translation else search_name final_search = show_translation[final_search] if self.is_show and final_search in show_translation else final_search final_search = get_tags_translation[final_search] if final_search in get_tags_translation else final_search @@ -854,7 +1562,9 @@ def get_search_choices(self, search_name, title=True, name_pairs=False): names = [] choices = {} use_title = title and final_search not in ["contentRating", "audioLanguage", "subtitleLanguage", "resolution"] - for choice in self.get_tags(final_search): + tags_iter = self.get_tags(final_search, person_list = person_list, tmdb_person_id = tmdb_person_id) + for choice in tags_iter: + if choice.title not in names: names.append((choice.title, choice.key) if name_pairs else choice.title) choices[choice.title] = choice.title if use_title else choice.key @@ -866,22 +1576,258 @@ def get_search_choices(self, search_name, title=True, name_pairs=False): logger.debug(f"Search Attribute: {final_search}") raise Failed(f"Plex Error: plex_search attribute: {search_name} not supported") - @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) - def get_tags(self, tag): + @retry(stop=stop_after_attempt(6), wait=wait_fixed(60), retry=retry_if_not_exception_type((BadRequest, NotFound, Unauthorized))) + def get_tags(self, tag, person_list=None, tmdb_person_id = None): if isinstance(tag, str): match = re.match(r'(?:([a-zA-Z]*)\.)?([a-zA-Z]+)', tag) if not match: raise BadRequest(f'Invalid filter field: {tag}') _libtype, tag = match.groups() - libtype = _libtype or self.Plex.TYPE - try: - tag = next(f for f in self.Plex.listFilters(libtype) if f.filter == tag) - except StopIteration: - available_filters = [f.filter for f in self.Plex.listFilters(libtype)] - raise NotFound(f'Unknown filter field "{tag}" for libtype "{libtype}". ' - f'Available filters: {available_filters}') from None + libtype = _libtype or self.lib_type # e.g. show + # libtype = _libtype or self.Plex.TYPE # e.g. show + + if not self.EmbyServer.is_in_filtertype(tag, libtype): + raise NotFound(f'Unknown filter field "{tag}" for libtype "{libtype}". ') from None + try: + tag = next(f for f in self.Plex.listFilters(libtype) if f.filter == tag) + except StopIteration: + available_filters = [f.filter for f in self.Plex.listFilters(libtype)] + raise NotFound(f'Unknown filter field "{tag}" for libtype "{libtype}". ' + f'Available filters: {available_filters}') from None + my_search = tag + else: + my_search = tag.filter + + # tag: <FilteringFilter:/library/sections/8/:Labels> + # items = {MediaContainer: 10} [<FilterChoice:284998:Overlay>, <FilterChoice:310126:Kometa>, <FilterChoice:310132:National-Film-Regist>, <FilterChoice:310150...-on-a-True-Stor>, <FilterChoice:310159:🎖Veteran's-Day-Movie>, <FilterChoice:310161:Seasonal>, <FilterChoice:310165:Top-Actors> + # TAG = {str} 'MediaContainer' + # TYPE = {NoneType} None + # allowSync = {int} 0 + # augmentationKey = {NoneType} None + # identifier = {str} 'com.plexapp.plugins.library' + # key = {NoneType} None + # librarySectionID = {NoneType} None + # librarySectionTitle = {NoneType} None + # librarySectionUUID = {NoneType} None + # mediaTagPrefix = {str} '/system/bundle/media/flags/' + # mediaTagVersion = {str} '1727455477' + # offset = {NoneType} None + # size = {int} 10 + # totalSize = {NoneType} None + # 00 = {FilterChoice} <FilterChoice:284998:Overlay> + # TAG = {str} 'Directory' + # TYPE = {NoneType} None + # fastKey = {str} '/library/sections/8/all?label=284998' + # key = {str} '284998' + # thumb = {NoneType} None + # title = {str} 'Overlay' + # type = {NoneType} None + # 01 = {FilterChoice} <FilterChoice:310126:Kometa> + # TAG = {str} 'Directory' + # TYPE = {NoneType} None + # fastKey = {str} '/library/sections/8/all?label=310126' + # key = {str} '310126' + # thumb = {NoneType} None + # title = {str} 'Kometa' + # type = {NoneType} None + + + # my_items = self.EmbyServer.get_collections_filter_choices() + + # tag = {FilteringFilter} <FilteringFilter:/library/sections/8/:Actor> + # TAG = {str} 'Filter' + # TYPE = {NoneType} None + # filter = {str} 'actor' + # filterType = {str} 'string' + # key = {str} '/library/sections/8/actor' + # title = {str} 'Actor' + # type = {str} 'filter' + + + + emby_items = [] + + if my_search in ['studio', 'show.studio']: # todo: differentiate between studio & network? + labels = self.EmbyServer.get_emby_studios(self, self.Emby.get("Id")) + + for label in labels: + key = str(label) + title = f"{str(label)}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + return emby_items + elif my_search in ['network', 'show.network']: # todo: differentiate between studio & network? + labels = self.EmbyServer.get_emby_networks(self, self.Emby.get("Id")) + + for label in labels: + key = str(label) + title = f"{str(label)}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + return emby_items + elif my_search in ['country']: + labels = self.EmbyServer.get_emby_countries(self.Emby.get("Id")) + + for label in labels: + key = str(label) + title = f"{str(label)}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + return emby_items + elif my_search in ['genre']: + genres = self.EmbyServer.get_emby_genres(self.Emby.get("Id")) + + for genre in genres: + key = str(genre) + title = f"{str(genre)}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + return emby_items + elif my_search in ['contentRating']: + content_ratings = self.EmbyServer.get_official_age_ratings(self.Emby.get("Id")) + + for rating in content_ratings: + key = rating.get("Name") + if key: + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=key) + emby_items.append(filter_choice) + return emby_items + elif my_search in ['label', 'show.label']: + labels = self.EmbyServer.get_emby_item_tags(self, self.Emby.get("Id"), search_all=True) + + labels += self.EmbyServer.get_emby_countries(self.Emby.get("Id")) + icon = '📺' if self.type == 'Show' else '🎥' + name = self.name + composed_name = f'{icon} {name} ' + for label in labels: + key = str(label) + title = f"{str(label)}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + if str(label).startswith(composed_name): + label_new = str(label).replace(composed_name, '') + key = str(label_new) + title = f"{str(label_new)}" + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + + return emby_items + elif my_search in ['decade']: + years = self.EmbyServer.get_years(self.Emby.get("Id")) + dekaden_set = set() + + for y in years: + jahr = int(y['Name']) + dekade = (jahr // 10) * 10 + dekaden_set.add(dekade) + + dekaden_liste = sorted(dekaden_set) + + for dec in dekaden_liste: + key = str(dec) + title = f"{str(dec)}s" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title) + emby_items.append(filter_choice) + return emby_items + # for decade in decades: + # key = decade + # title = f"{decade}s" + # 0 = {FilterChoice} <FilterChoice:2020:2020s> + # 1 = {FilterChoice} <FilterChoice:2010:2010s> + + # 0 = {FilterChoice} <FilterChoice:2020:2020s> + # TAG = {str} 'Directory' + # TYPE = {NoneType} None + # fastKey = {str} '/library/sections/8/all?decade=2020' + # key = {str} '2020' + # thumb = {NoneType} None + # title = {str} '2020s' + # type = {NoneType} None + + elif my_search in ["actor", "director", "writer", "producer", "composer"]: + + # short cut with proper tmdb id + if tmdb_person_id and len(person_list) == 1: + my_person = self.EmbyServer.get_person_info_bulk([tmdb_person_id], "tmdb") + my_choice = FilterChoiceEmby(key=my_person.get(int(tmdb_person_id)), title=person_list[0], thumb=None) + return [my_choice] + + emby_people = self.EmbyServer.get_people(my_search, person_list) + + for person in emby_people: + key = person.get('Id') + title = person.get('Name') + prov_ids = person.get('ProviderIds') + tmdb_id = prov_ids.get('Tmdb') if prov_ids else None + + # Construct the thumbnail URL + thumb = None + if 'ImageTags' in person and 'Primary' in person['ImageTags']: + server_url = self.EmbyServer.emby_server_url + image_tag = person['ImageTags']['Primary'] + thumb = f"{server_url}/Items/{key}/Images/Primary?tag={image_tag}" + + # Create a FilterChoiceEmby object + filter_choice = FilterChoiceEmby(key=key, title=title, thumb=thumb) + emby_items.append(filter_choice) + + # if len(emby_items) > 0: + return emby_items + elif my_search in ['resolution']: + my_dict = self.EmbyServer.get_resolutions() + return my_dict + # key = str(dec) + # title = f"{str(dec)}s" + # + # # Create a FilterChoiceEmby object + # filter_choice = FilterChoiceEmby(key=key, title=title) + # emby_items.append(filter_choice) + + + # Errors: + # 'country' + # country, region + continent not working + + raise Failed(f"Not implemented Emby search FilterChoice {tag}") + items = self.Plex.findItems(self.Plex._server.query(tag.key), FilterChoice) - if tag.key.endswith("/collection?type=4"): + + # { + # "Name": "Wolfgang Petersen", + # "ServerId": "37de8e11ee0748bea8d2080a13984949", + # "Id": "61041", + # "Type": "Person", + # "ImageTags": { + # "Primary": "ca733b3b975daa618201765a10805fe7" + # }, + # "BackdropImageTags": [] + # } + + # items_filter = object[FilterChoice]() + # key: '/library/sections/8/label' + # Plex: + # for elem in data: + # if self._checkAttrs(elem, **kwargs): + # item = self._buildItemOrNone(elem, cls, initpath) + # if item is not None: + # items.append(item) + # return items + + if tag.key.endswith("/collection?type=4"): # no idea keys = [k.key for k in items] keys.extend([k.key for k in self.Plex.findItems(self.Plex._server.query(f"{tag.key[:-1]}3"), FilterChoice)]) items = [i for i in self.Plex.findItems(self.Plex._server.query(tag.key[:-7]), FilterChoice) if i.key not in keys] @@ -937,13 +1883,14 @@ def manage_recommendations(self): r._data.attrib.get('promotedToOwnHome'), r._data.attrib.get('promotedToSharedHome')) for r in self.Plex.fetchItems(f"/hubs/sections/{self.Plex.key}/manage")] - def alter_collection(self, items, collection, smart_label_collection=False, add=True): + def alter_collection(self, items, collection, smart_label_collection=False, add=True, collection_id = None): maintain_status = True locked_items = [] unlocked_items = [] if not smart_label_collection and maintain_status and self.agent in ["tv.plex.agents.movie", "tv.plex.agents.series"]: for item in items: - item = self.reload(item) + # emby + # item = self.reload(item) if next((f for f in item.fields if f.name == "collection"), None) is not None: locked_items.append(item) else: @@ -953,14 +1900,54 @@ def alter_collection(self, items, collection, smart_label_collection=False, add= for _items, locked in [(locked_items, True), (unlocked_items, False)]: if _items: - self.Plex.batchMultiEdits(_items) + # Smart Label Collection (verwende JSON-basierte Labels) if smart_label_collection: - self.query_data(self.Plex.addLabel if add else self.Plex.removeLabel, collection) + if add: + # add / remove collection tag/label + # ToDo: Remove the tags for smart label collections + # if False: + for item in _items: + # Füge ein Label hinzu + self.EmbyServer.add_tags(item.ratingKey,[collection]) + + # add_label(kometa_labels, item.ratingKey, collection) + # save_labels_to_file(file_path_kometa, kometa_labels) + self.EmbyServer.add_remove_plex_object_from_collection(collection, _items, 'add') + + + else: + for item in _items: + # Entferne ein Label (JSON-basiert) + self.EmbyServer.remove_tags(item.ratingKey, [collection]) + # remove_label(kometa_labels, item.ratingKey, collection) + # save_labels_to_file(file_path_kometa, kometa_labels) + self.EmbyServer.add_remove_plex_object_from_collection(collection, _items, 'delete') + + + # Traditionelle Sammlungen (BoxSets in Emby) elif add: - self.Plex.addCollection(collection, locked=locked) + added = self.EmbyServer.add_to_collection(collection, [item.ratingKey for item in _items]) + # Sammlung erstellen oder Medien hinzufügen + if not added: + self.EmbyServer.create_collection(collection, [item.ratingKey for item in _items], locked=locked, parent_id= self.Emby.get("Id")) else: - self.Plex.removeCollection(collection, locked=locked) - self.Plex.saveMultiEdits() + # Tags entfernen und Sammlung löschen + for item in _items: + self.EmbyServer.remove_tags(item.ratingKey, [collection]) + # self.EmbyServer.remove_boxset(collection, collection_id) + self.EmbyServer.add_remove_plex_object_from_collection(collection, items, 'delete') + + # for _items, locked in [(locked_items, True), (unlocked_items, False)]: + # if _items: + # # self.Plex.batchMultiEdits(_items) + # if smart_label_collection: + # self.query_data(self.Plex.addLabel if add else self.Plex.removeLabel, collection) + # elif add: + # self.Plex.addCollection(collection, locked=locked) + # else: + # self.EmbyServer.remove_kometa_tags_from_collection(collection) + # self.Plex.removeCollection(collection, locked=locked) + # # self.Plex.saveMultiEdits() def move_item(self, collection, item, after=None): key = f"{collection.key}/items/{item}/move" @@ -969,7 +1956,17 @@ def move_item(self, collection, item, after=None): self._query(key, put=True) def smart_label_check(self, label): + + # print(f"Smart Label: {label}") + tags = self.EmbyServer.get_emby_item_tags(self, self.Emby.get("Id"), search_all=True,from_cache=False) + # + if label in tags: + return True + logger.trace(f"Label not found in Emby. Options: {tags}") + return False + labels = [la.title for la in self.get_tags("label")] # noqa + labels += self.EmbyServer.get_emby_countries(self.Emby.get("Id")) if label in labels: return True logger.trace(f"Label not found in Plex. Options: {labels}") @@ -981,9 +1978,26 @@ def test_smart_filter(self, uri_args): if len(test_items) < 1: raise Failed(f"Plex Error: No items for smart filter: {uri_args}") - def create_smart_collection(self, title, smart_type, uri_args, ignore_blank_results): + def create_smart_collection(self, title, smart_type, uri_args, ignore_blank_results, minimum = None): + + collection_id = self.EmbyServer.get_collection_id(title) + if collection_id: + return collection_id + if not ignore_blank_results: self.test_smart_filter(uri_args) + + # no smart collections in emby, using regular one + my_items = self.fetchItems(uri_args) + + if minimum and minimum > len(my_items): + return None + + + return self.EmbyServer.create_smart_collection(title, smart_type, my_items, ignore_blank_results, self.Emby.get("Id")) + # print(f"{smart_type} - {uri_args}") + + args = { "type": smart_type, "title": title, @@ -994,6 +2008,9 @@ def create_smart_collection(self, title, smart_type, uri_args, ignore_blank_resu self._query(f"/library/collections{utils.joinArgs(args)}", post=True) def create_blank_collection(self, title): + # Create a blank collection for Emby, add at least one title + return self.EmbyServer.create_collection(title,[self._emby_all_items[0].ratingKey], self.Emby.get("Id")) + args = { "type": 1 if self.is_movie else 2 if self.is_show else 8, "title": title, @@ -1011,9 +2028,73 @@ def get_smart_filter_from_uri(self, uri): def build_smart_filter(self, uri_args): return f"{self.PlexServer._uriRoot()}/library/sections/{self.Plex.key}/all{uri_args}" + def calculate_add_remove_items(self, new_items, current_items): + """ + Berechnet die Listen von Items, die hinzugefügt (add_items) oder entfernt (remove_items) werden müssen. + + :param new_items: Liste der neuen Items (z. B. aus fetchItems) + :param current_items: Liste der aktuellen Items in der Collection + :return: Tuple (add_items, remove_items) + """ + # Extrahiere die ratingKeys für schnellen Vergleich + new_keys = {item.ratingKey for item in new_items} + current_keys = {item.ratingKey for item in current_items} + + # Berechne Items, die hinzugefügt werden müssen (in new_items, aber nicht in current_items) + add_items = [item for item in new_items if item.ratingKey not in current_keys] + + # Berechne Items, die entfernt werden müssen (in current_items, aber nicht in new_items) + remove_items = [item for item in current_items if item.ratingKey not in new_keys] + + keep_items = [item for item in current_items if item.ratingKey in new_keys] + + return add_items, remove_items, keep_items + def update_smart_collection(self, collection, uri_args): self.test_smart_filter(uri_args) - self._query(f"/library/collections/{collection.ratingKey}/items{utils.joinArgs({'uri': self.build_smart_filter(uri_args)})}", put=True) + + new_items = self.fetchItems(uri_args) + current_items = collection.items() + add_items, remove_items, keep_items = self.calculate_add_remove_items(new_items, current_items) + + + # return + + logger.info("") + logger.separator(f"Syncing SmartEmby Collection {collection.title} {self.type}", space=False, border=False) + logger.info("") + + + total = len(add_items) + len(remove_items) + spacing = len(str(total)) * 2 + 1 + + # Prüfe auf hinzugefügte oder unveränderte Items + for i, item in enumerate(add_items, 1): + current_operation = "+" + number_text = f"{i}/{total}" + logger.info( + f"{number_text:>{spacing}} | {collection.title} {self.type} | {current_operation} | {util.item_title(item)}") + + # Prüfe auf hinzugefügte oder unveränderte Items + for i, item in enumerate(remove_items, 1): + current_operation = "-" + number_text = f"{i + len(add_items)}/{total}" + logger.info( + f"{number_text:>{spacing}} | {collection.title} {self.type} | {current_operation} | {util.item_title(item)}") + + if len(remove_items) >0: + self.EmbyServer.add_remove_plex_object_from_collection(collection.title, remove_items, 'delete', collection_id = collection.ratingKey) + # print(f"Removed {len(remove_items)} from Emby {collection.title} ") + if len(add_items) >0: + self.EmbyServer.add_remove_plex_object_from_collection(collection.title, add_items, 'add', collection_id = collection.ratingKey) + # print(f"Added {len(add_items)} to Emby {collection.title} ") + + logger.exorcise() + logger.info("") + item_label = f"{self.type.capitalize()}{'s' if total > 1 else ''}" + logger.info(f"{total} {item_label} Processed - Added {len(add_items)} {item_label} labels, Removed {len(remove_items)} {item_label} labels") + + # self._query(f"/library/collections/{collection.ratingKey}/items{utils.joinArgs({'uri': self.build_smart_filter(uri_args)})}", put=True) def smart_filter(self, collection): smart_filter = self.get_collection(collection).content @@ -1061,16 +2142,33 @@ def get_collection(self, data, force_search=False, debug=True): elif isinstance(data, int) and not force_search: return self.fetchItem(data) else: - cols = self.search(title=str(data), libtype="collection") - for d in cols: - if d.title == data: - return d + lib_id = self.Emby.get("Id") + # my_cols = self.EmbyServer.get_boxsets_from_library(str(data), library_id=lib_id ) + # my_col = self.EmbyServer.get_boxsets_from_library(str(data)) + col_id= self.EmbyServer.get_collection_id(str(data)) + if col_id: + emby_col = self.EmbyServer.get_item(col_id) + return self.EmbyServer.convert_emby_to_plex([emby_col])[0] + + # Rest fails + raise Failed(f"Emby Error: Collection {data} not found") + if col_id: + my_cols = self.EmbyServer.get_boxset_by_title(str(data)) + # print(my_cols) + if len(my_cols) > 0: + return my_cols[0] + if debug: logger.debug("") - for d in cols: + for d in my_cols: logger.debug(f"Found: {d.title}") logger.debug(f"Looking for: {data}") - raise Failed(f"Plex Error: Collection {data} not found") + + # return empty list + # return None + raise Failed(f"Emby Error: Collection {data} not found") + + def validate_collections(self, collections): valid_collections = [] @@ -1187,7 +2285,7 @@ def get_rating_keys(self, method, data, is_playlist=False): for i, item in enumerate(all_items, 1): logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") add_item = True - item = self.reload(item, force=True) + # item = self.reload(item) for collection in item.collections: if str(collection.tag).lower() in collection_indexes: add_item = False @@ -1198,19 +2296,42 @@ def get_rating_keys(self, method, data, is_playlist=False): else: raise Failed(f"Plex Error: Method {method} not supported") if not items: - raise Failed("Plex Error: No Items found in Plex") + # raise Failed("Plex Error: No Items found in Plex") + return[] return [(item.ratingKey, "ratingKey") for item in items] def get_collection_items(self, collection, smart_label_collection): + # print(f"{collection} - {smart_label_collection}") + if smart_label_collection: + my_collection= None + if hasattr(collection, 'ratingKey'): + my_collection = collection.ratingKey + else: + my_collection:str = self.EmbyServer.get_collection_id(collection if isinstance(collection, str) else collection.title ) + if my_collection: + return self.EmbyServer.get_items_in_boxset(my_collection) + return [] + + # self.create_blank_collection(collection) + # my_collection: str = self.EmbyServer.get_collection_id(collection) + # return self.EmbyServer.get_items_in_boxset(my_collection) + return self.search(label=collection.title if isinstance(collection, Collection) else str(collection)) elif isinstance(collection, (Collection, Playlist)): if collection.smart: return self.fetchItems(self.smart_filter(collection)) else: - return self.query(collection.items) - else: - return [] + my_items = self.EmbyServer.get_items_in_boxset(collection.ratingKey) + # my_return = self.query(collection.items) + return my_items + elif isinstance(collection, str): + mycol = self.EmbyServer.get_collection_id(collection) + if mycol: + my_items = self.EmbyServer.get_items_in_boxset(mycol) + return self.EmbyServer.convert_emby_to_plex(my_items) + + return [] def get_collection_name_and_items(self, collection, smart_label_collection): name = collection.title if isinstance(collection, (Collection, Playlist)) else str(collection) @@ -1244,6 +2365,61 @@ def edit_advance(self, item, edits): return False def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True, locked=True, is_locked=None): + + + display = "" + final = "" + key = attribute_translation[attr] if attr in attribute_translation else attr + actual = "similar" if attr == "similar_artist" else attr + attr_display = attr.replace("_", " ").title() + + if add_tags or remove_tags or sync_tags is not None: + _add_tags = add_tags if add_tags else [] + _remove_tags = remove_tags if remove_tags else [] + _sync_tags = sync_tags if sync_tags else [] + + if attr == "label": + _item_tags = self.EmbyServer.get_emby_item_tags(obj,self.Emby.get("Id"), from_cache=False) + elif attr == "genre": + _item_tags = self.EmbyServer.get_emby_item_genres(obj,self.Emby.get("Id"), from_cache=False) + else: + pass + + _add = [t for t in _add_tags + _sync_tags if t not in _item_tags] + _remove = [t for t in _item_tags if (sync_tags is not None and t not in _sync_tags) or t in _remove_tags] + + # Berechne die finalen Tags + final_tags = sorted(set([t for t in _item_tags if t not in _remove] + _add)) + final_tags = sorted(set(final_tags)) # Entferne eventuelle Duplikate + if final_tags != sorted(set(_item_tags)): + if attr == "label": + self.EmbyServer.set_tags(obj.ratingKey, final_tags) + elif attr == "genre": + self.EmbyServer.set_genres(obj.ratingKey, final_tags) + else: + raise WARNING(f"edit_tags: I won't edit {attr} with {final_tags}") + + if _add: + display += f"+{', +'.join(_add)}" + if _remove: + if display: + display += ", " + display += f"-{', -'.join(_remove)}" + if is_locked is not None and not display and is_locked != locked: + # self.edit_query(obj, {f"{actual}.locked": 1 if locked else 0}) + # todo: add emby locked? + display = "Locked" if locked else "Unlocked" + final = f"{obj.title[:25]:<25} | {attr_display} | {display}" if display else display + if do_print and final: + logger.info(final) + return final[28:] if final else final + + # if add_tags and not remove_tags and not None: + # self.EmbyServer.add_tags(obj.ratingKey, add_tags) + # return + raise WARNING(f"EMBY EDIT TAGS: {self} - {attr} - {obj} - {add_tags} - {remove_tags} - {sync_tags} - { locked} - {is_locked}") + + display = "" final = "" key = attribute_translation[attr] if attr in attribute_translation else attr @@ -1316,6 +2492,8 @@ def image_update(self, item, image, tmdb=None, title=None, poster=True): except BadRequest as e: logger.stacktrace() logger.error(f"Plex Error: {e}") + # todo: check for file, no overlay in tags + # if poster and "Overlay" in self.item_labels(item): if poster and "Overlay" in [la.tag for la in self.item_labels(item)]: logger.info(self.edit_tags("label", item, remove_tags="Overlay", do_print=False)) else: @@ -1324,7 +2502,7 @@ def image_update(self, item, image, tmdb=None, title=None, poster=True): def item_images(self, item, group, alias, initial=False, asset_location=None, asset_directory=None, title=None, image_name=None, folder_name=None, style_data=None): if title is None: title = item.title - posters, backgrounds = util.get_image_dicts(group, alias) + posters, backgrounds, logos = util.get_image_dicts(group, alias) if style_data and "url_poster" in style_data and style_data["url_poster"]: posters["style_data"] = style_data["url_poster"] elif style_data and "tpdb_poster" in style_data and style_data["tpdb_poster"]: @@ -1333,8 +2511,10 @@ def item_images(self, item, group, alias, initial=False, asset_location=None, as backgrounds["style_data"] = style_data["url_background"] elif style_data and "tpdb_background" in style_data and style_data["tpdb_background"]: backgrounds["style_data"] = f"https://theposterdb.com/api/assets/{style_data['tpdb_background']}" + if style_data and "url_logo" in style_data and style_data["url_logo"]: + logos["style_data"] = style_data["url_logo"] try: - asset_poster, asset_background, item_dir, folder_name = self.find_item_assets(item, item_asset_directory=asset_location, asset_directory=asset_directory) + asset_poster, asset_background, asset_logo, item_dir, folder_name = self.find_item_assets(item, item_asset_directory=asset_location, asset_directory=asset_directory) if asset_poster: posters["asset_directory"] = asset_poster if asset_background: @@ -1345,11 +2525,13 @@ def item_images(self, item, group, alias, initial=False, asset_location=None, as logger.warning(e) poster = self.pick_image(title, posters, self.prioritize_assets, self.download_url_assets, asset_location, image_name=image_name) background = self.pick_image(title, backgrounds, self.prioritize_assets, self.download_url_assets, asset_location, - is_poster=False, image_name=f"{image_name}_background" if image_name else image_name) + image_type="background", image_name=f"{image_name}_background" if image_name else image_name) + logo = self.pick_image(title, logos, self.prioritize_assets, self.download_url_assets, asset_location, + image_type="logo", image_name=f"{image_name}_logo" if image_name else image_name) updated = False - if poster or background: - pu, bu = self.upload_images(item, poster=poster, background=background, overlay=True) - if pu or bu: + if poster or background or logo: + pu, bu, lu = self.upload_images(item, poster=poster, background=background, logo=logo, overlay=True) + if pu or bu or lu: updated = True return asset_location, folder_name, updated @@ -1357,10 +2539,10 @@ def find_and_upload_assets(self, item, current_labels, asset_directory=None): item_dir = None name = None try: - poster, background, item_dir, name = self.find_item_assets(item, asset_directory=asset_directory) + poster, background, logo, item_dir, name = self.find_item_assets(item, asset_directory=asset_directory) if "Overlay" not in current_labels: - if poster or background: - self.upload_images(item, poster=poster, background=background) + if poster or background or logo: + self.upload_images(item, poster=poster, background=background, logo=logo) elif self.show_missing_assets: logger.warning(f"Asset Warning: No poster or background found in the assets folder '{item_dir}'") else: @@ -1375,23 +2557,25 @@ def find_and_upload_assets(self, item, current_labels, asset_directory=None): found_episode = False for season in self.query(item.seasons): try: - season_poster, season_background, _, _ = self.find_item_assets(season, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) + season_poster, season_background, season_logo, _, _ = self.find_item_assets(season, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) if season_poster: found_season = True elif self.show_missing_season_assets and season.seasonNumber > 0: missing_seasons += f"\nMissing Season {season.seasonNumber} Poster" - if season_poster or season_background and "Overlay" not in [la.tag for la in self.item_labels(season)]: - self.upload_images(season, poster=season_poster, background=season_background) + if season_poster or season_background or season_logo and "Overlay" not in [la.tag for la in self.item_labels(season)]: + self.upload_images(season, poster=season_poster, background=season_background, logo=season_logo) except Failed as e: if self.show_missing_assets: logger.warning(e) for episode in self.query(season.episodes): try: if episode.seasonEpisode: - episode_poster, episode_background, _, _ = self.find_item_assets(episode, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) - if episode_poster or episode_background: + episode_poster, episode_background, episode_logo, _, _ = self.find_item_assets(episode, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) + if episode_poster or episode_background or episode_logo: found_episode = True - if "Overlay" not in [la.tag for la in self.item_labels(episode)]: + # if "Overlay" not in [la.tag for la in self.item_labels(episode)]: + #todo: check for file, no overlay in tags + if "Overlay" not in self.item_labels(episode): self.upload_images(episode, poster=episode_poster, background=episode_background) elif self.show_missing_episode_assets: missing_episodes += f"\nMissing {episode.seasonEpisode.upper()} Title Card" @@ -1405,7 +2589,7 @@ def find_and_upload_assets(self, item, current_labels, asset_directory=None): found_album = False for album in self.query(item.albums): try: - album_poster, album_background, _, _ = self.find_item_assets(album, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) + album_poster, album_background, _, _, _ = self.find_item_assets(album, item_asset_directory=item_dir, asset_directory=asset_directory, folder_name=name) if album_poster or album_background: found_album = True elif self.show_missing_season_assets: @@ -1421,6 +2605,7 @@ def find_and_upload_assets(self, item, current_labels, asset_directory=None): def find_item_assets(self, item, item_asset_directory=None, asset_directory=None, folder_name=None): poster = None background = None + logo = None if asset_directory is None: asset_directory = self.asset_directory @@ -1447,9 +2632,14 @@ def find_item_assets(self, item, item_asset_directory=None, asset_directory=None starting = item.artist() else: starting = item - if not starting.locations: + emby_item = self.EmbyServer.get_item(starting.ratingKey) + emby_path_media = emby_item.get('Path', None) + # directory_path = os.path.dirname(emby_path_media) + # starting.locations.append(directory_path) + + if not emby_path_media: raise Failed(f"Asset Warning: No video filepath found for {item.title}") - path_test = str(starting.locations[0]) + path_test = str(emby_path_media) if not os.path.dirname(path_test): path_test = path_test.replace("\\", "/") folder_name = os.path.basename(os.path.dirname(path_test) if isinstance(starting, Movie) else path_test) @@ -1490,10 +2680,11 @@ def find_item_assets(self, item, item_asset_directory=None, asset_directory=None logger.warning(f"Asset Warning: Asset Directory Not Found and Created: {item_asset_directory}") else: raise Failed(f"Asset Warning: Unable to find asset folder: '{folder_name}'") - return None, None, item_asset_directory, folder_name + return None, None, None, item_asset_directory, folder_name poster_filter = os.path.join(item_asset_directory, f"{file_name}.*") background_filter = os.path.join(item_asset_directory, "background.*" if file_name == "poster" else f"{file_name}_background.*") + logo_filter = os.path.join(item_asset_directory, "logo.*" if file_name == "poster" else f"{file_name}_logo.*") poster_matches = util.glob_filter(poster_filter) if len(poster_matches) > 0: @@ -1501,7 +2692,11 @@ def find_item_assets(self, item, item_asset_directory=None, asset_directory=None background_matches = util.glob_filter(background_filter) if len(background_matches) > 0: - background = ImageData("asset_directory", os.path.abspath(background_matches[0]), prefix=prefix, is_poster=False, is_url=False) + background = ImageData("asset_directory", os.path.abspath(background_matches[0]), prefix=prefix, image_type="background", is_url=False) + + logo_matches = util.glob_filter(logo_filter) + if len(logo_matches) > 0: + logo = ImageData("asset_directory", os.path.abspath(logo_matches[0]), prefix=prefix, image_type="logo", is_url=False) if is_top_level and self.asset_folders and self.dimensional_asset_rename and (not poster or not background): for file in util.glob_filter(os.path.join(item_asset_directory, "*.*")): @@ -1516,18 +2711,27 @@ def find_item_assets(self, item, item_asset_directory=None, asset_directory=None elif not background and _w > _h: new_path = os.path.join(os.path.dirname(file), f"background{os.path.splitext(file)[1].lower()}") os.rename(file, new_path) - background = ImageData("asset_directory", os.path.abspath(new_path), prefix=prefix, is_poster=False, is_url=False) + background = ImageData("asset_directory", os.path.abspath(new_path), prefix=prefix, image_type="background", is_url=False) if poster and background: break except OSError: logger.error(f"Asset Error: Failed to open image: {file}") - return poster, background, item_asset_directory, folder_name + return poster, background, logo, item_asset_directory, folder_name def get_ids(self, item): tmdb_id = None tvdb_id = None imdb_id = None + + provider_ids = self.EmbyServer.get_provider_ids(item) + emby_imdb_id = provider_ids[0] + emby_tvdb_id = provider_ids[1] + emby_tmdb_id = provider_ids[2] + + return emby_tmdb_id, emby_tvdb_id, emby_imdb_id + + if self.config.Cache: t_id, i_id, guid_media_type, _ = self.config.Cache.query_guid_map(item.guid) if t_id: @@ -1537,13 +2741,76 @@ def get_ids(self, item): tvdb_id = t_id[0] if i_id: imdb_id = i_id[0] - if not tmdb_id and not tvdb_id: - tmdb_id = self.get_tmdb_from_map(item) - if not tmdb_id and not tvdb_id and self.is_show: - tvdb_id = self.get_tvdb_from_map(item) - if not imdb_id: - imdb_id = self.get_imdb_from_map(item) - return tmdb_id, tvdb_id, imdb_id + return tmdb_id, tvdb_id, imdb_id + + def upload_images(self, item, poster=None, background=None, logo=None, overlay=False): + poster_uploaded = False + if poster is not None: + try: + emby_item = self.EmbyServer.get_item(item.ratingKey) + emby_images = emby_item.get("ImageTags") + emby_poster_compare = emby_images.get("Primary") if emby_images else None + image_compare = None + if self.config.Cache: + _, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name) + if not image_compare or str(f"{poster.compare}-{emby_poster_compare}") != str(image_compare): + if overlay: + # self.reload(item, force=True) + if overlay and "Overlay" in [la.tag for la in self.item_labels(item)]: + item.removeLabel("Overlay") + poster_uploaded = self._upload_image(item, poster) + logger.info(f"Metadata: {poster.attribute} updated {poster.message}") + elif self.show_asset_not_needed: + logger.info(f"Metadata: {poster.prefix}poster update not needed") + except Failed: + logger.stacktrace() + logger.error(f"Metadata: {poster.attribute} failed to update {poster.message}") + + background_uploaded = False + if background is not None: + try: + image_compare = None + if self.config.Cache: + _, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds") + if not image_compare or str(background.compare) != str(image_compare): + background_uploaded = self._upload_image(item, background) + logger.info(f"Metadata: {background.attribute} updated {background.message}") + elif self.show_asset_not_needed: + logger.info(f"Metadata: {background.prefix}background update not needed") + except Failed: + logger.stacktrace() + logger.error(f"Metadata: {background.attribute} failed to update {background.message}") + + logo_uploaded = False + if logo is not None: + try: + image_compare = None + if self.config.Cache: + _, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_logos") + if not image_compare or str(logo.compare) != str(image_compare): + logo_uploaded = self._upload_image(item, logo) + logger.info(f"Metadata: {logo.attribute} updated {logo.message}") + elif self.show_asset_not_needed: + logger.info(f"Metadata: {logo.prefix}logo update not needed") + except Failed: + logger.stacktrace() + logger.error(f"Metadata: {logo.attribute} failed to update {logo.message}") + + if self.config.Cache: + + if poster_uploaded: + emby_item = self.EmbyServer.get_item(item.ratingKey, force_refresh=True) + emby_images = emby_item.get("ImageTags") + emby_poster_compare = emby_images.get("Primary") if emby_images else None + + self.config.Cache.update_image_map(item.ratingKey, self.image_table_name, "", f"{poster.compare}-{emby_poster_compare}" if poster else "") + if background_uploaded: + self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", "", background.compare) + if logo_uploaded: + self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_logos", "", logo.compare) + + return poster_uploaded, background_uploaded, logo_uploaded + def get_ratings(self, item): ratings = { @@ -1567,7 +2834,7 @@ def get_ratings(self, item): def get_locked_attributes(self, item, titles=None, year_titles=None, item_type=None): if not item_type: item_type = self.type - item = self.reload(item) + # item = self.reload(item) attrs = {} match_dict = {} fields = {f.name: f for f in item.fields if f.locked} @@ -1637,6 +2904,7 @@ def check_field(plex_key, kometa_key, var_key=None): check_field("genre", "genre", var_key="genres") check_field("writer", "writer", var_key="writers") check_field("producer", "producer", var_key="producers") + check_field("composer", "composer", var_key="composers") check_field("collection", "collection", var_key="collections") check_field("label", "label", var_key="labels") check_field("mood", "mood", var_key="moods") @@ -1675,13 +2943,30 @@ def _recur(sub, item_type_in=None): return map_key, attrs def get_item_display_title(self, item_to_sort, sort=False): + if isinstance(item_to_sort, Album): return f"{item_to_sort.artist().titleSort if sort else item_to_sort.parentTitle} Album {item_to_sort.titleSort if sort else item_to_sort.title}" elif isinstance(item_to_sort, Season): - return f"{item_to_sort.show().titleSort if sort else item_to_sort.parentTitle} Season {item_to_sort.seasonNumber}" + titleSort = None + if sort: + season = self.EmbyServer.get_item(item_to_sort.ratingKey) + show = self.EmbyServer.get_item(season.get("SeriesId")) + titleSort = show.get("SortName") + return f"{titleSort if sort else item_to_sort.parentTitle} Season {item_to_sort.seasonNumber}" elif isinstance(item_to_sort, Episode): + titleSort = None + if sort: + season = self.EmbyServer.get_item(item_to_sort.ratingKey) + show = self.EmbyServer.get_item(season.get("SeriesId")) + titleSort = show.get("SortName") + # ToDo - Not working / tested return f"{item_to_sort.show().titleSort if sort else item_to_sort.grandparentTitle} {item_to_sort.seasonEpisode.upper()}" else: + key = item_to_sort.ratingKey if not (isinstance(item_to_sort, int) or isinstance(item_to_sort, str)) else str(item_to_sort) + # must bei str Id + item = self.EmbyServer.get_item(key) + return item.get("SortName") if sort else item.get("Name") + return item_to_sort.titleSort if sort else item_to_sort.title def split(self, text): @@ -1711,6 +2996,11 @@ def check_filters(self, item, filters_in, current_time): def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, current_time): filter_actual = attribute_translation[filter_attr] if filter_attr in attribute_translation else filter_attr + if item.ratingKey in self.filter_items_cache: + emby_item = self.filter_items_cache[item.ratingKey] + else: + emby_item = self.EmbyServer.get_item(item.ratingKey) + self.filter_items_cache[item.ratingKey] = emby_item if isinstance(item, Movie): item_type = "movie" elif isinstance(item, Show): @@ -1727,13 +3017,14 @@ def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, c item_type = "track" else: return True - item = self.reload(item) + # item = self.reload(item) if filter_attr not in builder.filters[item_type]: return True elif filter_attr in builder.date_filters: if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, current_time): return False elif filter_attr in builder.string_filters: + #ToDo: most of the stuff for proprietary Emby item not integrated yet values = [] if filter_attr == "audio_track_title": for media in item.media: @@ -1749,7 +3040,8 @@ def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, c if attr and attr not in values: values.append(attr) elif filter_attr in ["filepath", "folder"]: - values = [loc for loc in item.locations if loc] + values = [emby_item.get("Path")] + # values = [loc for loc in item.locations if loc] else: test_value = getattr(item, filter_actual) values = [test_value] if test_value else [] @@ -1757,30 +3049,46 @@ def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, c return False elif filter_attr in builder.boolean_filters: filter_check = False + # logger.info(f"filter attribute: {filter_attr}") if filter_attr == "has_collection": filter_check = len(item.collections) > 0 - elif filter_attr == "has_edition": + elif filter_attr == "has_edition": # no edition in Emby, filename regex filter_check = True if item.editionTitle else False elif filter_attr == "has_stinger": filter_check = False if item.ratingKey in self.movie_rating_key_map and self.movie_rating_key_map[item.ratingKey] in self.config.mediastingers: filter_check = True elif filter_attr == "has_overlay": + if os.path.exists(os.path.join(self.overlay_destination_folder, item.ratingKey, ".png")): + filter_check = True for label in self.item_labels(item): if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay": filter_check = True break elif filter_attr == "has_dolby_vision": - for media in item.media: - for part in media.parts: - for stream in part.videoStreams(): - if stream.DOVIPresent: - filter_check = True - break + media_sources = emby_item.get("MediaSources", []) + # logger.warning(f"My media: {media_sources}") + + for media in media_sources: + media_streams = media.get("MediaStreams", []) + for stream in media_streams: + if stream.get("VideoRange") == "DolbyVision": + # logger.warning("Found Dolby Vision!") + filter_check = True + break + + # for media in item.media: + # for part in media.parts: + # for stream in part.videoStreams(): + # if stream.DOVIPresent: + # filter_check = True + # break if util.is_boolean_filter(filter_data, filter_check): return False elif filter_attr == "history": - item_date = item.originallyAvailableAt + my_date = emby_item.get("PremiereDate") + item_date = datetime.fromisoformat(my_date.replace("Z", "+00:00")) + # item_date = item.originallyAvailableAt if item_date is None: return False elif filter_data == "day": @@ -1805,7 +3113,9 @@ def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, c elif filter_attr == "tracks": sub_items = item.tracks() else: - sub_items = item.episodes() + episodes = self.EmbyServer.get_items({"ParentId": item.ratingKey}, include_item_types="Episode") + sub_items = self.EmbyServer.convert_emby_to_plex(episodes) + # sub_items = item.episodes() filters_in = [] percentage = 60 for sub_atr, sub_data in filter_data.items(): @@ -1868,7 +3178,7 @@ def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, c attrs.extend([s.language, s.languageCode]) elif filter_attr in ["content_rating", "year", "rating"]: attrs = [getattr(item, filter_actual)] - elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", + elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "composer", "writer", "collection", "network"]: attrs = [attr.tag for attr in getattr(item, filter_actual)] else: diff --git a/modules/poster.py b/modules/poster.py index 47b100e68..d3d7d394d 100644 --- a/modules/poster.py +++ b/modules/poster.py @@ -6,14 +6,16 @@ logger = util.logger class ImageData: - def __init__(self, attribute, location, prefix="", is_poster=True, is_url=True, compare=None): + def __init__(self, attribute, location, prefix="", image_type="poster", is_url=True, compare=None): self.attribute = attribute self.location = location self.prefix = prefix - self.is_poster = is_poster + self.is_poster = image_type == "poster" + self.is_background = image_type == "background" + self.is_logo = image_type == "logo" self.is_url = is_url - self.compare = compare if compare else location if is_url else os.stat(location).st_size - self.message = f"{prefix}{'poster' if is_poster else 'background'} to [{'URL' if is_url else 'File'}] {location}" + self.compare = compare if compare else location if is_url else os.stat(location).st_size if os.path.exists(location) else None + self.message = f"{prefix}{image_type} to [{'URL' if is_url else 'File'}] {location}" def __str__(self): return str(self.__dict__) @@ -392,5 +394,5 @@ def save(self, item_vars): pmm_image.save(image_path) - return ImageData(self.image_attr, image_path, is_url=False, compare=self.get_compare_string()) + return ImageData(self.image_attr, image_path, is_url=False, image_type="poster", compare=self.get_compare_string()) diff --git a/modules/request.py b/modules/request.py index e7cff9350..5fb481b15 100644 --- a/modules/request.py +++ b/modules/request.py @@ -6,6 +6,7 @@ from requests.exceptions import ConnectionError from tenacity import retry, stop_after_attempt, wait_fixed from urllib import parse +from urllib.parse import unquote, parse_qsl logger = util.logger @@ -36,6 +37,25 @@ def parse_qs(data): return parse.parse_qs(data) + +def parse_query_with_plus(uri_args: str): + """ + Parses a query string while preserving '+' as a literal character. + :param uri_args: The query string to parse. + :return: A dictionary of parsed query arguments. + """ + # Entfernt führendes '?' und parst die Query-Parameter + parsed_args = parse_qsl(uri_args.lstrip('?'), keep_blank_values=True) + + # Rekonstruiert ein Dictionary + result = {} + for key, value in parsed_args: + # Manuelles Dekodieren von Schlüssel und Wert, wobei '+' nicht ersetzt wird + key = unquote(key) + value = unquote(value.replace('+', '%2B')) # Behandle "+" explizit + result[key] = value + + return result def urlparse(data): return parse.urlparse(str(data)) @@ -90,7 +110,7 @@ def no_verify_ssl(self, session=None): import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - def download_image(self, title, image_url, download_directory, session=None, is_poster=True, filename=None): + def download_image(self, title, image_url, download_directory, session=None, image_type="poster", filename=None): response = self.get_image(image_url, session=session) new_image = os.path.join(download_directory, f"{filename}") if filename else download_directory if response.headers["Content-Type"] == "image/jpeg": @@ -101,7 +121,7 @@ def download_image(self, title, image_url, download_directory, session=None, is_ new_image += ".png" with open(new_image, "wb") as handler: handler.write(response.content) - return ImageData("asset_directory", new_image, prefix=f"{title}'s ", is_poster=is_poster, is_url=False) + return ImageData("asset_directory", new_image, prefix=f"{title}'s ", image_type=image_type, is_url=False) def file_yaml(self, path_to_file, check_empty=False, create=False, start_empty=False): return YAML(path=path_to_file, check_empty=check_empty, create=create, start_empty=start_empty) @@ -143,7 +163,7 @@ def get_stream(self, url, location, info="Item"): logger.exorcise() def get_scrape_html(self, url): - html.fromstring(self.scraper.get(url).content) + return html.fromstring(self.scraper.get(url).content) def get_html(self, url, headers=None, params=None, header=None, language=None): return html.fromstring(self.get(url, headers=headers, params=params, header=header, language=language).content) diff --git a/modules/tmdb.py b/modules/tmdb.py index fe924a87c..17b61dfe5 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -1,4 +1,6 @@ import re +from typing import Any + from modules import util from modules.util import Failed from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_not_exception_type @@ -104,23 +106,54 @@ def _load(self, data): class TMDbMovie(TMDBObj): + def __init__(self, tmdb, tmdb_id, ignore_cache=False): super().__init__(tmdb, tmdb_id, ignore_cache=ignore_cache) expired = None data = None + refreshed=False if self._tmdb.cache and not ignore_cache: data, expired = self._tmdb.cache.query_tmdb_movie(tmdb_id, self._tmdb.expiration) if expired or not data: data = self.load_movie() + refreshed=True super()._load(data) + self.cast = [] + self.crew =[] + + if isinstance(data, dict): + self.cast = data.get("cast") + self.crew = data.get("crew") + else: # tmdb object + all_cast = data.cast + all_crew = data.crew + if all_cast: + for actor_entry in all_cast: + self.cast.append({ + "person_id": actor_entry.person_id, + "name": actor_entry.name, + "character": actor_entry.character + }) + if all_crew: + for member in all_crew: + self.crew.append({ + "person_id": member.person_id, + "name": member.name, + "job": member.job, + "department": member.department, + }) + + if self.crew: + pass + self.original_title = data["original_title"] if isinstance(data, dict) else data.original_title self.release_date = data["release_date"] if isinstance(data, dict) else data.release_date self.studio = data["studio"] if isinstance(data, dict) else data.companies[0].name if data.companies else None self.collection_id = data["collection_id"] if isinstance(data, dict) else data.collection.id if data.collection else None self.collection_name = data["collection_name"] if isinstance(data, dict) else data.collection.name if data.collection else None - if self._tmdb.cache and not ignore_cache: + if refreshed and self._tmdb.cache and not ignore_cache: self._tmdb.cache.update_tmdb_movie(expired, self, self._tmdb.expiration) @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type(Failed)) @@ -137,6 +170,7 @@ def load_movie(self): class TMDbShow(TMDBObj): def __init__(self, tmdb, tmdb_id, ignore_cache=False): super().__init__(tmdb, tmdb_id, ignore_cache=ignore_cache) + self.tmdb_cache = {} expired = None data = None if self._tmdb.cache and not ignore_cache: @@ -163,7 +197,9 @@ def __init__(self, tmdb, tmdb_id, ignore_cache=False): @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type(Failed)) def load_show(self): try: - return self._tmdb.TMDb.tv_show(self.tmdb_id, partial="external_ids,keywords") + if self.tmdb_id not in self.tmdb_cache: + self.tmdb_cache[self.tmdb_id] = self._tmdb.TMDb.tv_show(self.tmdb_id, partial="external_ids,keywords") + return self.tmdb_cache[self.tmdb_id] except NotFound: raise Failed(f"TMDb Error: No Show found for TMDb ID: {self.tmdb_id}") except TMDbException as e: @@ -433,7 +469,7 @@ def get_tmdb_ids(self, method, data, is_movie, region): logger.info(f"Processing {pretty}: ({tmdb_id}) {tmdb_name} ({len(ids)} Item{'' if len(ids) == 1 else 's'})") return ids - def get_item(self, item, tmdb_id, tvdb_id, imdb_id, is_movie=True): + def get_item(self, item, tmdb_id, tvdb_id, imdb_id, is_movie=True,ignore_cache =False): tmdb_item = None if tvdb_id and not tmdb_id: tmdb_id = self.config.Convert.tvdb_to_tmdb(tvdb_id) @@ -443,7 +479,7 @@ def get_item(self, item, tmdb_id, tvdb_id, imdb_id, is_movie=True): tmdb_id = _id if tmdb_id: try: - tmdb_item = self.get_movie(tmdb_id) if is_movie else self.get_show(tmdb_id) + tmdb_item = self.get_movie(tmdb_id,ignore_cache ) if is_movie else self.get_show(tmdb_id) except Failed as e: logger.error(str(e)) elif tvdb_id and not is_movie: diff --git a/modules/trakt.py b/modules/trakt.py index 0c6726cc1..61015590c 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -42,11 +42,12 @@ def __init__(self, requests, read_only, params): self.read_only = read_only self.client_id = params["client_id"] self.client_secret = params["client_secret"] + self.force_refresh = params["force_refresh"] self.pin = params["pin"] self.config_path = params["config_path"] self.authorization = params["authorization"] logger.secret(self.client_secret) - if not self._save(self.authorization): + if self.force_refresh is True or not self._save(self.authorization): if not self._refresh(): self._authorization() self._slugs = None @@ -225,16 +226,22 @@ def _request(self, url, params=None, json_data=None): response = self.requests.get(f"{base_url}{url}", headers=headers, params=params) if pages == 1 and "X-Pagination-Page-Count" in response.headers and not params: pages = int(response.headers["X-Pagination-Page-Count"]) + if response.status_code == 401: + if not self._refresh(): + logger.debug(f"Trakt token refresh failure") + raise Failed(f"({response.status_code}) {response.reason}") if response.status_code >= 400: + logger.debug(f"Trakt response issue: ({response.status_code}) {response.reason}") raise Failed(f"({response.status_code}) {response.reason}") - response_json = response.json() - logger.trace(f"Headers: {response.headers}") - logger.trace(f"Response: {response_json}") - if isinstance(response_json, dict): - return response_json else: - output_json.extend(response_json) - current += 1 + response_json = response.json() + logger.trace(f"Headers: {response.headers}") + logger.trace(f"Response: {response_json}") + if isinstance(response_json, dict): + return response_json + else: + output_json.extend(response_json) + current += 1 return output_json def user_ratings(self, is_movie): @@ -259,11 +266,21 @@ def convert(self, external_id, from_source, to_source, media_type): return lookup[0][media_type]["ids"][to_source] raise Failed(f"Trakt Error: No {to_source.upper().replace('B', 'b')} ID found for {from_source.upper().replace('B', 'b')} ID: {external_id}") - def list_description(self, data): + def list_description(self, list_url): + if "/official/" in list_url: + try: + data = self._request(urlparse(list_url).path.replace("/official/", "/")) + except Failed: + raise Failed(list_url) + if "ids" not in data or "trakt" not in data["ids"] or not data["ids"]["trakt"]: + raise Failed(f"Trakt Error: Could not extract ID for official list {list_url}") + path = f"/lists/{data['ids']['trakt']}" + else: + path = urlparse(list_url).path try: - return self._request(urlparse(data).path)["description"] + return self._request(path)["description"] except Failed: - raise Failed(data) + raise Failed(list_url) def _parse(self, items, typeless=False, item_type=None, trakt_ids=False, ignore_other=False): ids = [] diff --git a/modules/tvdb.py b/modules/tvdb.py index 1675e9c28..3f14fb66d 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -61,8 +61,8 @@ def __init__(self, tvdb, tvdb_id, is_movie=False, ignore_cache=False): def parse_page(xpath, is_list=False): parse_results = data.xpath(xpath) if len(parse_results) > 0: - parse_results = [r.strip() for r in parse_results if len(r) > 0] - return parse_results if is_list else parse_results[0] if len(parse_results) > 0 else None + parse_results = [r.strip() for r in parse_results if isinstance(r, str) and len(r.strip()) > 0] + return parse_results if is_list else (parse_results[0] if len(parse_results) > 0 else None) def parse_title_summary(lang=None): place = "//div[@class='change_translation_text' and " @@ -77,7 +77,14 @@ def parse_title_summary(lang=None): self.release_date = data["release_date"] self.status = data["status"] self.genres = data["genres"].split("|") + + # neu: direkt als String aus DB (evtl. leer) + self.networks = data.get("networks", "") or "" + self.production = data.get("production", "") or "" + self.studio = data.get("studio", "") or "" + else: + # HTML/DOM scrapen self.title, self.summary = parse_title_summary(lang=self._tvdb.language) if not self.title and self._tvdb.language in language_translation: self.title, self.summary = parse_title_summary(lang=language_translation[self._tvdb.language]) @@ -101,8 +108,28 @@ def parse_title_summary(lang=None): self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]", is_list=True) - if self._tvdb.cache and not ignore_cache: - self._tvdb.cache.update_tvdb(expired, self, self._tvdb.expiration) + # neu: Networks / Production / Studio extrahieren + networks_list = [] if is_movie else ( + parse_page("//strong[(text()='Network' or text()='Networks')]/parent::li/span/a/text()[normalize-space()]", + is_list=True) or [] + ) + production_list = parse_page( + "//strong[(text()='Production Company' or text()='Production Companies')]/parent::li/span/a/text()[normalize-space()]", + is_list=True + ) or [] + studio_list = parse_page( + "//strong[(text()='Studio' or text()='Studios')]/parent::li/span/a/text()[normalize-space()]", + is_list=True + ) or [] + + # Als Strings mit |-Separator ablegen (wie bei genres in der DB) + self.networks = "|".join(networks_list) if networks_list else "" + self.production = "|".join(production_list) if production_list else "" + self.studio = "|".join(studio_list) if studio_list else "" + + # 4) Cache aktualisieren (schreibt auch networks/production/studio in tvdb_data5) + if self._tvdb.cache and not ignore_cache: + self._tvdb.cache.update_tvdb(expired, self, self._tvdb.expiration) class TVDb: def __init__(self, requests, cache, tvdb_language, expiration): @@ -115,6 +142,10 @@ def get_tvdb_obj(self, tvdb_url, is_movie=False): tvdb_id, _, _ = self.get_id_from_url(tvdb_url, is_movie=is_movie) return TVDbObj(self, tvdb_id, is_movie=is_movie) + def get_tvdb_obj_from_id(self, tvdb_id, is_movie=False): + # tvdb_id, _, _ = self.get_id_from_url(tvdb_url, is_movie=is_movie) + return TVDbObj(self, tvdb_id, is_movie=is_movie) + @retry(stop=stop_after_attempt(6), wait=wait_fixed(10), retry=retry_if_not_exception_type(Failed)) def get_request(self, tvdb_url): response = self.requests.get(tvdb_url, language=self.language) diff --git a/modules/util.py b/modules/util.py index 30b00b47c..2a54accd6 100644 --- a/modules/util.py +++ b/modules/util.py @@ -100,8 +100,8 @@ def __call__(self, retry_state): "Artist": ["album_sorting"] } tags_to_edit = { - "Movie": ["genre", "label", "collection", "country", "director", "producer", "writer"], - "Video": ["genre", "label", "collection", "country", "director", "producer", "writer"], + "Movie": ["genre", "label", "collection", "country", "director", "producer", "composer", "writer"], + "Video": ["genre", "label", "collection", "country", "director", "producer", "composer", "writer"], "Show": ["genre", "label", "collection"], "Artist": ["genre", "label", "style", "mood", "country", "collection", "similar_artist"] } @@ -121,17 +121,20 @@ def __call__(self, retry_state): def get_image_dicts(group, alias): posters = {} backgrounds = {} + logos = {} - for attr in ["url_poster", "file_poster", "url_background", "file_background"]: + for attr in ["url_poster", "file_poster", "url_background", "file_background", "file_logo", "url_logo"]: if attr in alias: if group[alias[attr]]: if "poster" in attr: posters[attr] = group[alias[attr]] - else: + elif "background" in attr: backgrounds[attr] = group[alias[attr]] + else: + logos[attr] = group[alias[attr]] else: logger.error(f"Metadata Error: {attr} attribute is blank") - return posters, backgrounds + return posters, backgrounds, logos def add_dict_list(keys, value, dict_map): for key in keys: @@ -250,9 +253,10 @@ def validate_filename(filename): mapping_name = sanitize_filename(str(filename)) return mapping_name, f"Log Folder Name: {filename} is invalid using {mapping_name}" + def item_title(item): if isinstance(item, Season): - if f"Season {item.index}" == item.title: + if f"Season {item.index}" == item.title or f"Staffel {item.index}" == item.title: return f"{item.parentTitle} {item.title}" else: return f"{item.parentTitle} Season {item.index}: {item.title}" diff --git a/requirements.txt b/requirements.txt index d15706e5b..f5d7c59f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,19 @@ arrapi==1.4.13 cloudscraper==1.2.71 -GitPython==3.1.44 -lxml==5.3.1 +GitPython==3.1.45 +lxml==6.0.0 num2words==0.5.14 -pathvalidate==3.2.3 -pillow==11.1.0 -PlexAPI==4.16.1 +pathvalidate==3.3.1 +pillow==11.3.0 +PlexAPI==4.17.0 psutil==7.0.0 python-dateutil==2.9.0.post0 -python-dotenv==1.1.0 -pywin32==310; sys_platform == 'win32' -requests==2.32.3 -ruamel.yaml==0.18.10 +python-dotenv==1.1.1 +pywin32==311; sys_platform == 'win32' +requests==2.32.4 +ruamel.yaml==0.18.14 schedule==1.2.2 -setuptools==78.1.0 -tenacity==9.0.0 -tmdbapis==1.2.28 \ No newline at end of file +setuptools==80.9.0 +tenacity==9.1.2 +tmdbapis==1.2.28 +deepdiff==8.6.0