Merge pull request #783 from meisnate12/develop

v1.16.2
pull/790/head v1.16.2
meisnate12 3 years ago committed by GitHub
commit 278a674bda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,11 +11,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check Out Repo
uses: actions/checkout@v3
- name: Send Discord Release Notification - name: Send Discord Release Notification
uses: bythope/discord-webhook-messages@v1.1.0 uses: nhevia/discord-styled-releases@main
with: with:
webhookUrl: ${{ secrets.RELEASE_WEBHOOK }} webhook_id: ${{ secrets.RELEASE_WEBHOOK_ID }}
handler: 'release' webhook_token: ${{ secrets.RELEASE_WEBHOOK_TOKEN }}

@ -1,18 +1,20 @@
FROM python:3.9-slim FROM python:3.9-slim
ENV TINI_VERSION v0.19.0 ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini COPY . /
RUN chmod +x /tini
RUN echo "**** install system packages ****" \ RUN echo "**** install system packages ****" \
&& apt-get update \ && apt-get update \
&& apt-get upgrade -y --no-install-recommends \ && apt-get upgrade -y --no-install-recommends \
&& apt-get install -y tzdata --no-install-recommends \ && apt-get install -y tzdata --no-install-recommends \
&& apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev && apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev wget \
COPY requirements.txt / && wget -O /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-"$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
RUN echo "**** install python packages ****" \ && chmod +x /tini \
&& pip3 install --no-cache-dir --upgrade --requirement /requirements.txt \ && pip3 install --no-cache-dir --upgrade --requirement /requirements.txt \
&& apt-get autoremove -y \ && apt-get --purge autoremove wget gcc g++ libxml2-dev libxslt-dev libz-dev -y \
&& apt-get clean \ && apt-get clean \
&& apt-get update \
&& apt-get check \
&& apt-get -f install \
&& apt-get autoclean \
&& rm -rf /requirements.txt /tmp/* /var/tmp/* /var/lib/apt/lists/* && rm -rf /requirements.txt /tmp/* /var/tmp/* /var/lib/apt/lists/*
COPY . /
VOLUME /config VOLUME /config
ENTRYPOINT ["/tini", "-s", "python3", "plex_meta_manager.py", "--"] ENTRYPOINT ["/tini", "-s", "python3", "plex_meta_manager.py", "--"]

@ -1 +1 @@
1.16.1 1.16.2

@ -169,3 +169,7 @@ p {
.tab-set { .tab-set {
width: auto !important; width: auto !important;
} }
.highlighted {
background-color: #ac0ce3 !important;
color: #FFFFFF !important;
}

@ -16,32 +16,35 @@ libraries:
The available attributes for the operations attribute are as follows The available attributes for the operations attribute are as follows
| Attribute | Description | | Attribute | Description |
|:--------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `assets_for_all` | Search in assets for images for every item in your library.<br>**Values:** `true` or `false` | | `assets_for_all` | Search in assets for images for every item in your library.<br>**Values:** `true` or `false` |
| `delete_collections_with_less` | Deletes every collection with less than the given number of items.<br>**Values:** number greater then 0 | | `delete_collections_with_less` | Deletes every collection with less than the given number of items.<br>**Values:** number greater then 0 |
| `delete_unmanaged_collections` | Deletes every unmanaged collection<br>**Values:** `true` or `false` | | `delete_unmanaged_collections` | Deletes every unmanaged collection<br>**Values:** `true` or `false` |
| `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb for Genres</td></tr><tr><td>`tvdb`</td><td>Use TVDb for Genres</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Genres</td></tr></table> | | `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb for Genres</td></tr><tr><td>`tvdb`</td><td>Use TVDb for Genres</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Genres</td></tr><tr><td>`anidb`</td><td>Use AniDB Tags for Genres</td></tr></table> |
| `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`mdb`</td><td>Use MdbList for Content Ratings</td></tr><tr><td>`mdb_commonsense`</td><td>Use Commonsense Rating through MDbList for Content Ratings</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Content Ratings</td></tr></table> | | `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`mdb`</td><td>Use MdbList for Content Ratings</td></tr><tr><td>`mdb_commonsense`</td><td>Use Commonsense Rating through MDbList for Content Ratings</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Content Ratings</td></tr></table> |
| `mass_audience_rating_update`/<br>`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Rating</td></tr><tr><td>`omdb`</td><td>Use IMDbRating through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Score</td></tr><tr><td>`mdb_imdb`</td><td>Use IMDb Rating through MDbList</td></tr><tr><td>`mdb_metacritic`</td><td>Use Metacritic Rating through MDbList</td></tr><tr><td>`mdb_metacriticuser`</td><td>Use Metacritic User Rating through MDbList</td></tr><tr><td>`mdb_trakt`</td><td>Use Trakt Rating through MDbList</td></tr><tr><td>`mdb_tomatoes`</td><td>Use Rotten Tomatoes Rating through MDbList</td></tr><tr><td>`mdb_tomatoesaudience`</td><td>Use Rotten Tomatoes Audience Rating through MDbList</td></tr><tr><td>`mdb_tmdb`</td><td>Use TMDb Rating through MDbList</td></tr><tr><td>`mdb_letterboxd`</td><td>Use Letterboxd Rating through MDbList</td></tr></table> | | `mass_originally_available_update` | Updates every item's originally available date in the library to the chosen site's date<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Release Date</td></tr><tr><td>`tvdb`</td><td>Use TVDb Release Date</td></tr><tr><td>`omdb`</td><td>Use IMDb Release Date through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Release Date</td></tr><tr><td>`anidb`</td><td>Use AniDB Release Date</td></tr></table> |
| `mass_trakt_rating_update` | Updates every movie/show's user rating in the library to match your custom rating on Trakt if there is one<br>**Values:** `true` or `false` | | `mass_audience_rating_update`/<br>`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Rating</td></tr><tr><td>`omdb`</td><td>Use IMDbRating through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Score</td></tr><tr><td>`mdb_imdb`</td><td>Use IMDb Rating through MDbList</td></tr><tr><td>`mdb_metacritic`</td><td>Use Metacritic Rating through MDbList</td></tr><tr><td>`mdb_metacriticuser`</td><td>Use Metacritic User Rating through MDbList</td></tr><tr><td>`mdb_trakt`</td><td>Use Trakt Rating through MDbList</td></tr><tr><td>`mdb_tomatoes`</td><td>Use Rotten Tomatoes Rating through MDbList</td></tr><tr><td>`mdb_tomatoesaudience`</td><td>Use Rotten Tomatoes Audience Rating through MDbList</td></tr><tr><td>`mdb_tmdb`</td><td>Use TMDb Rating through MDbList</td></tr><tr><td>`mdb_letterboxd`</td><td>Use Letterboxd Rating through MDbList</td></tr><tr><td>`anidb_rating`</td><td>Use AniDB Rating</td></tr><tr><td>`anidb_average`</td><td>Use AniDB Average</td></tr></table> |
| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> | | `mass_imdb_parental_labels` | Updates every item's labels in the library to match the IMDb Parental Guide<br>**Values** `with_none` or `without_none` |
| `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` | | `mass_trakt_rating_update` | Updates every movie/show's user rating in the library to match your custom rating on Trakt if there is one<br>**Values:** `true` or `false` |
| `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` | | `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> |
| `radarr_add_all` | Adds every item in the library to 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) details to convert the paths.<br>**Values:** `true` or `false` | | `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` |
| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags | | `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` |
| `sonarr_add_all` | Adds every item in the library to 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) details to convert the paths.<br>**Values:** `true` or `false` | | `radarr_add_all` | Adds every item in the library to 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) details to convert the paths.<br>**Values:** `true` or `false` |
| `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags | | `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags |
| `genre_mapper` | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | | `sonarr_add_all` | Adds every item in the library to 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) details to convert the paths.<br>**Values:** `true` or `false` |
| `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | | `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags |
| [`genre_mapper`](#genre-mapper) | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) |
| [`content_rating_mapper`](#content-rating-mapper) | Allows content ratings to be changed to other content ratings or be removed from every item in your library.<br>**Values:** [see below for usage](#content-rating-mapper) |
| [`metadata_backup`](#metadata-backup) | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) |
## Genre Mapper ## Genre Mapper
You can use the `genre_mapper` operation to map genres in your library. You can use the `genre_mapper` operation to map genres in your library.
Each attribute under `genre_mapper` is a separate mapping and has two parts. Each attribute under `genre_mapper` is a separate mapping and has two parts.
* The key (`Action` in the example below) is what the genres will end up as. * The key (`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the value.
* The value(`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the key. * The value (`Action` in the example below) is what the genres will end up as.
So 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`. So 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`.
@ -50,36 +53,58 @@ library:
Movies: Movies:
operations: operations:
genre_mapper: genre_mapper:
Action: Action/Adventure, Action & Adventure "Action/Adventure": Action
Comedy: Romantic Comedy "Action & Adventure": Action
Romantic Comedy: Comedy
``` ```
you can also use a list: To just Remove a Genre without replacing it just set the Genre to nothing like this.
```yaml ```yaml
library: library:
Movies: Movies:
operations: operations:
genre_mapper: genre_mapper:
Action: "Action/Adventure": Action
- Action/Adventure "Action & Adventure": Action
- Action & Adventure Romantic Comedy:
Comedy: Romantic Comedy
``` ```
To just Remove a Genre without replacing it just set the Genre to nothing like this. This 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
You can use the `content_rating_mapper` operation to map content ratings in your library.
Each attribute under `content_rating_mapper` is a separate mapping and has two parts.
* The key (`PG`, `PG-13` in the example below) is what content ratings you want mapped to the value.
* The value (`Y-10` in the example below) is what the content ratings will end up as.
So 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 ```yaml
library: library:
Movies: Movies:
operations: operations:
genre_mapper: content_rating_mapper:
Action: Action/Adventure, Action & Adventure PG: Y-10
Romantic Comedy: "PG-13": Y-10
R: Y-17
``` ```
This 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`. To just Remove a content rating without replacing it just set the content rating to nothing like this.
```yaml
library:
Movies:
operations:
content_rating_mapper:
PG: Y-10
"PG-13": Y-10
R:
```
This 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 ## Metadata Backup

@ -11,9 +11,10 @@ tmdb:
language: en language: en
``` ```
| Attribute | Allowed Values | Default | Required | | Attribute | Allowed Values | Default | Required |
|:-----------|:---------------------|:-------:|:--------:| |:-------------------|:--------------------------------------------------------------------------|:-------:|:--------:|
| `apikey` | User TMDb V3 API Key | N/A | &#9989; | | `apikey` | User TMDb V3 API Key | N/A | &#9989; |
| `language` | User Language | en | &#10060; | | `language` | User Language | en | &#10060; |
| `cache_expiration` | Number of days before each cache mapping expires and has to be re-cached. | 60 | &#10060; |
If you do not have a TMDb V3 API key please refer to this [guide](https://developers.themoviedb.org/3/getting-started/introduction). If you do not have a TMDb V3 API key please refer to this [guide](https://developers.themoviedb.org/3/getting-started/introduction).

@ -12,6 +12,7 @@ No configuration is required for these builders.
| [`anilist_relations`](#anilist-relations) | Finds the anime specified by the AniList ID and every relation in its relation tree except Character and Other relations | &#9989; | &#9989; | &#10060; | | [`anilist_relations`](#anilist-relations) | Finds the anime specified by the AniList ID and every relation in its relation tree except Character and Other relations | &#9989; | &#9989; | &#10060; |
| [`anilist_studio`](#anilist-studio) | Finds all anime specified by the AniList Studio ID | &#9989; | &#9989; | &#10060; | | [`anilist_studio`](#anilist-studio) | Finds all anime specified by the AniList Studio ID | &#9989; | &#9989; | &#10060; |
| [`anilist_id`](#anilist-id) | Finds the anime specified by the AniList ID | &#9989; | &#9989; | &#10060; | | [`anilist_id`](#anilist-id) | Finds the anime specified by the AniList ID | &#9989; | &#9989; | &#10060; |
| [`anilist_userlist`](#anilist-userlist) | Finds the anime in AniList User's Anime list the options are detailed below | &#9989; | &#9989; | &#9989; |
| [`anilist_search`](#anilist-search) | Finds the anime specified by the AniList search parameters provided | &#9989; | &#9989; | &#9989; | | [`anilist_search`](#anilist-search) | Finds the anime specified by the AniList search parameters provided | &#9989; | &#9989; | &#9989; |
## AniList Top Rated Anime ## AniList Top Rated Anime
@ -98,6 +99,31 @@ collections:
anilist_id: 23, 219 anilist_id: 23, 219
``` ```
## 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` Details 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 |
| `sort_by` | **Description:** Sort Order to return<br>**Default:** `score`<br>**Values:**<table class="clearTable"><tr><td>`score`</td><td>Sort by User Score</td></tr><tr><td>`popularity`</td><td>Sort by Popularity</td></tr><tr><td>`status`</td><td>Sort by Status</td></tr><tr><td>`progress`</td><td>Sort by Progress</td></tr><tr><td>`last_updated`</td><td>Sort by Last Updated</td></tr><tr><td>`last_added`</td><td>Sort by Last Added</td></tr><tr><td>`start_date`</td><td>Sort by Start Date</td></tr><tr><td>`completed_date`</td><td>Sort by Completed Date</td></tr></table> |
```yaml
collections:
Currently Watching Anime:
anilist_userlist:
username: Username
list_name: Watching
sort_by: score
collection_order: custom
sync_mode: sync
```
## AniList Search ## AniList Search
Finds the anime specified by the AniList Search the options are detailed below. Finds the anime specified by the AniList Search the options are detailed below.

@ -1538,6 +1538,10 @@ dynamic_collections:
Name of the template to use for these dynamic collections. Each `type` has its own default template, but if you want to define and use your own template you can. Name of the template to use for these dynamic collections. Each `type` has its own default template, but if you want to define and use your own template you can.
Each template is passed a template variable whose name matches the dynamic collection `type`. i.e. in the example below `<<network>>` is the template variable.
`key` and `key_name` are both passed along and can be used as template variables.
For example, the template below removes the limit on the `smart_filter` so it shows all items in each network For example, the template below removes the limit on the `smart_filter` so it shows all items in each network
```yaml ```yaml

@ -76,7 +76,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**.
| `resolution` | Uses the resolution tag to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `resolution` | Uses the resolution tag to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `audio_language` | Uses the audio language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `audio_language` | Uses the audio language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `original_language`<sup>1</sup> | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match<br>Example: `original_language: en, ko` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `original_language`<sup>1</sup> | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match<br>Example: `original_language: en, ko` | &#10060; | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; |
| `origin_country`<sup>1</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `origin_country`<sup>1</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_status`<sup>1</sup> | Uses TMDb Status to match<br>**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `tmdb_status`<sup>1</sup> | Uses TMDb Status to match<br>**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_type`<sup>1</sup> | Uses TMDb Type to match<br>**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `tmdb_type`<sup>1</sup> | Uses TMDb Type to match<br>**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |

@ -1,4 +1,5 @@
import time import time
from datetime import datetime
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
@ -14,9 +15,51 @@ urls = {
"login": f"{base_url}/perl-bin/animedb.pl" "login": f"{base_url}/perl-bin/animedb.pl"
} }
class AniDBObj:
def __init__(self, anidb, anidb_id, language):
self.anidb = anidb
self.anidb_id = anidb_id
self.language = language
response = self.anidb._request(f"{urls['anime']}/{anidb_id}", language=self.language)
def parse_page(xpath, is_list=False, is_float=False, is_date=False, fail=False):
parse_results = response.xpath(xpath)
try:
if len(parse_results) > 0:
parse_results = [r.strip() for r in parse_results if len(r) > 0]
if parse_results:
if is_list:
return parse_results
elif is_float:
return float(parse_results[0])
elif is_date:
return datetime.strptime(parse_results[0], "%d.%m.%Y")
else:
return parse_results[0]
except (ValueError, TypeError):
pass
if fail:
raise Failed(f"AniDB Error: No Anime Found for AniDB ID: {self.anidb_id}")
elif is_list:
return []
elif is_float:
return 0
else:
return None
self.official_title = parse_page(f"//th[text()='Main Title']/parent::tr/td/span/text()", fail=True)
self.title = parse_page(f"//th[text()='Official Title']/parent::tr/td/span/span/span[text()='{self.language}']/parent::span/parent::span/parent::td/label/text()")
self.rating = parse_page(f"//th[text()='Rating']/parent::tr/td/span/a/span/text()", is_float=True)
self.average = parse_page(f"//th[text()='Average']/parent::tr/td/span/a/span/text()", is_float=True)
self.released = parse_page(f"//th[text()='Year']/parent::tr/td/span/text()", is_date=True)
self.tags = [g.capitalize() for g in parse_page("//th/a[text()='Tags']/parent::th/parent::tr/td/span/a/span/text()", is_list=True)]
self.description = response.xpath(f"string(//div[@itemprop='description'])")
class AniDB: class AniDB:
def __init__(self, config): def __init__(self, config, language):
self.config = config self.config = config
self.language = language
self.username = None self.username = None
self.password = None self.password = None
@ -29,46 +72,46 @@ class AniDB:
if not self._request(urls["login"], data=data).xpath("//li[@class='sub-menu my']/@title"): if not self._request(urls["login"], data=data).xpath("//li[@class='sub-menu my']/@title"):
raise Failed("AniDB Error: Login failed") raise Failed("AniDB Error: Login failed")
def _request(self, url, language=None, data=None): def _request(self, url, data=None):
if self.config.trace_mode: if self.config.trace_mode:
logger.debug(f"URL: {url}") logger.debug(f"URL: {url}")
if data: if data:
return self.config.post_html(url, data=data, headers=util.header(language)) return self.config.post_html(url, data=data, headers=util.header(self.language))
else: else:
return self.config.get_html(url, headers=util.header(language)) return self.config.get_html(url, headers=util.header(self.language))
def _popular(self, language): def _popular(self):
response = self._request(urls["popular"], language=language) response = self._request(urls["popular"])
return util.get_int_list(response.xpath("//td[@class='name anime']/a/@href"), "AniDB ID") return util.get_int_list(response.xpath("//td[@class='name anime']/a/@href"), "AniDB ID")
def _relations(self, anidb_id, language): def _relations(self, anidb_id):
response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}", language=language) response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}")
return util.get_int_list(response.xpath("//area/@href"), "AniDB ID") return util.get_int_list(response.xpath("//area/@href"), "AniDB ID")
def _validate(self, anidb_id, language): def _validate(self, anidb_id):
response = self._request(f"{urls['anime']}/{anidb_id}", language=language) response = self._request(f"{urls['anime']}/{anidb_id}")
ids = response.xpath(f"//*[text()='a{anidb_id}']/text()") ids = response.xpath(f"//*[text()='a{anidb_id}']/text()")
if len(ids) > 0: if len(ids) > 0:
return util.regex_first_int(ids[0], "AniDB ID") return util.regex_first_int(ids[0], "AniDB ID")
raise Failed(f"AniDB Error: AniDB ID: {anidb_id} not found") raise Failed(f"AniDB Error: AniDB ID: {anidb_id} not found")
def validate_anidb_ids(self, anidb_ids, language): def validate_anidb_ids(self, anidb_ids):
anidb_list = util.get_int_list(anidb_ids, "AniDB ID") anidb_list = util.get_int_list(anidb_ids, "AniDB ID")
anidb_values = [] anidb_values = []
for anidb_id in anidb_list: for anidb_id in anidb_list:
try: try:
anidb_values.append(self._validate(anidb_id, language)) anidb_values.append(self._validate(anidb_id))
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
if len(anidb_values) > 0: if len(anidb_values) > 0:
return anidb_values return anidb_values
raise Failed(f"AniDB Error: No valid AniDB IDs in {anidb_list}") raise Failed(f"AniDB Error: No valid AniDB IDs in {anidb_list}")
def _tag(self, tag, limit, language): def _tag(self, tag, limit):
anidb_ids = [] anidb_ids = []
current_url = f"{urls['tag']}/{tag}" current_url = f"{urls['tag']}/{tag}"
while True: while True:
response = self._request(current_url, language=language) response = self._request(current_url)
anidb_ids.extend(util.get_int_list(response.xpath("//td[@class='name main anime']/a/@href"), "AniDB ID")) anidb_ids.extend(util.get_int_list(response.xpath("//td[@class='name main anime']/a/@href"), "AniDB ID"))
next_page_list = response.xpath("//li[@class='next']/a/@href") next_page_list = response.xpath("//li[@class='next']/a/@href")
if len(anidb_ids) >= limit or len(next_page_list) == 0: if len(anidb_ids) >= limit or len(next_page_list) == 0:
@ -77,20 +120,23 @@ class AniDB:
current_url = f"{base_url}{next_page_list[0]}" current_url = f"{base_url}{next_page_list[0]}"
return anidb_ids[:limit] return anidb_ids[:limit]
def get_anidb_ids(self, method, data, language): def get_anime(self, anidb_id):
return AniDBObj(self, anidb_id, self.language)
def get_anidb_ids(self, method, data):
anidb_ids = [] anidb_ids = []
if method == "anidb_popular": if method == "anidb_popular":
logger.info(f"Processing AniDB Popular: {data} Anime") logger.info(f"Processing AniDB Popular: {data} Anime")
anidb_ids.extend(self._popular(language)[:data]) anidb_ids.extend(self._popular()[:data])
elif method == "anidb_tag": elif method == "anidb_tag":
logger.info(f"Processing AniDB Tag: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag ID: {data['tag']}") logger.info(f"Processing AniDB Tag: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag ID: {data['tag']}")
anidb_ids = self._tag(data["tag"], data["limit"], language) anidb_ids = self._tag(data["tag"], data["limit"])
elif method == "anidb_id": elif method == "anidb_id":
logger.info(f"Processing AniDB ID: {data}") logger.info(f"Processing AniDB ID: {data}")
anidb_ids.append(data) anidb_ids.append(data)
elif method == "anidb_relation": elif method == "anidb_relation":
logger.info(f"Processing AniDB Relation: {data}") logger.info(f"Processing AniDB Relation: {data}")
anidb_ids.extend(self._relations(data, language)) anidb_ids.extend(self._relations(data))
else: else:
raise Failed(f"AniDB Error: Method {method} not supported") raise Failed(f"AniDB Error: Method {method} not supported")
logger.debug("") logger.debug("")

@ -4,8 +4,12 @@ from modules.util import Failed
logger = util.logger logger = util.logger
builders = ["anilist_id", "anilist_popular", "anilist_trending", "anilist_relations", "anilist_studio", "anilist_top_rated", "anilist_search"] builders = ["anilist_id", "anilist_popular", "anilist_trending", "anilist_relations", "anilist_studio", "anilist_top_rated", "anilist_search", "anilist_userlist"]
pretty_names = {"score": "Average Score", "popular": "Popularity", "trending": "Trending"} pretty_names = {"score": "Average Score", "popular": "Popularity", "trending": "Trending"}
pretty_user = {
"status": "Status", "score": "User Score", "progress": "Progress", "last_updated": "Last Updated",
"last_added": "Last Added", "start_date": "Start Date", "completed_date": "Completed Date", "popularity": "Popularity"
}
attr_translation = { attr_translation = {
"year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory", "year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory",
"score": "averageScore", "min_tag_percent": "minimumTagRank", "country": "countryOfOrigin", "score": "averageScore", "min_tag_percent": "minimumTagRank", "country": "countryOfOrigin",
@ -20,6 +24,11 @@ mod_searches = [
no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent", "limit", "sort_by", "source", "country"] no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent", "limit", "sort_by", "source", "country"]
searches = mod_searches + no_mod_searches searches = mod_searches + no_mod_searches
sort_options = {"score": "SCORE_DESC", "popular": "POPULARITY_DESC", "trending": "TRENDING_DESC"} sort_options = {"score": "SCORE_DESC", "popular": "POPULARITY_DESC", "trending": "TRENDING_DESC"}
userlist_sort_options = {
"score": "SCORE_DESC", "status": "STATUS_DESC", "progress": "PROGRESS_DESC",
"last_updated": "UPDATED_TIME_DESC", "last_added": "ADDED_TIME_DESC", "start_date": "STARTED_ON_DESC",
"completed_date": "FINISHED_ON_DESC", "popularity": "MEDIA_POPULARITY_DESC"
}
media_season = {"winter": "WINTER", "spring": "SPRING", "summer": "SUMMER", "fall": "FALL"} media_season = {"winter": "WINTER", "spring": "SPRING", "summer": "SUMMER", "fall": "FALL"}
media_format = {"tv": "TV", "short": "TV_SHORT", "movie": "MOVIE", "special": "SPECIAL", "ova": "OVA", "ona": "ONA", "music": "MUSIC"} media_format = {"tv": "TV", "short": "TV_SHORT", "movie": "MOVIE", "special": "SPECIAL", "ova": "OVA", "ona": "ONA", "music": "MUSIC"}
media_status = {"finished": "FINISHED", "airing": "RELEASING", "not_yet_aired": "NOT_YET_RELEASED", "cancelled": "CANCELLED", "hiatus": "HIATUS"} media_status = {"finished": "FINISHED", "airing": "RELEASING", "not_yet_aired": "NOT_YET_RELEASED", "cancelled": "CANCELLED", "hiatus": "HIATUS"}
@ -210,6 +219,44 @@ class AniList:
return anilist_ids, ignore_ids, name return anilist_ids, ignore_ids, name
def _userlist(self, username, list_name, sort_by):
query = """
query ($user: String, $sort: [MediaListSort]) {
MediaListCollection (userName: $user, sort: $sort, type: ANIME) {
lists {
name
entries {
media{id}
}
}
}
}
"""
variables = {"user": username, "sort": userlist_sort_options[sort_by]}
for alist in self._request(query, variables)["data"]["MediaListCollection"]["lists"]:
if alist["name"] == list_name:
return [m["media"]["id"] for m in alist["entries"]]
return []
def validate_userlist(self, data):
query = """
query ($user: String) {
MediaListCollection (userName: $user, type: ANIME) {
lists {name}
}
}
"""
variables = {"user": data["username"]}
json_obj = self._request(query, variables)
if not json_obj["data"]["MediaListCollection"]:
raise Failed(f"AniList Error: User: {data['username']} not found")
list_names = [n["name"] for n in json_obj["data"]["MediaListCollection"]["lists"]]
if not list_names:
raise Failed(f"AniList Error: User: {data['username']} has no Lists")
if data["list_name"] in list_names:
return data
raise Failed(f"AniList Error: List: {data['list_name']} not found\nOptions: {', '.join(list_names)}")
def validate(self, name, data): def validate(self, name, data):
valid = [] valid = []
for d in util.get_list(data): for d in util.get_list(data):
@ -243,6 +290,9 @@ class AniList:
elif method == "anilist_relations": elif method == "anilist_relations":
anilist_ids, _, name = self._relations(data) anilist_ids, _, name = self._relations(data)
logger.info(f"Processing AniList Relations: ({data}) {name} ({len(anilist_ids)} Anime)") logger.info(f"Processing AniList Relations: ({data}) {name} ({len(anilist_ids)} Anime)")
elif method == "anilist_userlist":
anilist_ids = self._userlist(data["username"], data["list_name"], data["sort_by"])
logger.info(f"Processing AniList Userlist: {data['list_name']} from {data['username']} sorted by {pretty_user[data['sort_by']]}")
else: else:
if method == "anilist_popular": if method == "anilist_popular":
data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"} data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"}

@ -169,7 +169,7 @@ all_filters = boolean_filters + special_filters + \
smart_invalid = ["collection_order", "collection_level"] smart_invalid = ["collection_order", "collection_level"]
smart_url_invalid = ["minimum_items", "filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details smart_url_invalid = ["minimum_items", "filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details
custom_sort_builders = [ custom_sort_builders = [
"plex_search", "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated", "plex_search", "plex_pilots", "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated",
"tmdb_trending_daily", "tmdb_trending_weekly", "tmdb_discover", "tmdb_trending_daily", "tmdb_trending_weekly", "tmdb_discover",
"tvdb_list", "imdb_chart", "imdb_list", "stevenlu_popular", "anidb_popular", "tvdb_list", "imdb_chart", "imdb_list", "stevenlu_popular", "anidb_popular",
"trakt_list", "trakt_watchlist", "trakt_collection", "trakt_trending", "trakt_popular", "trakt_boxoffice", "trakt_list", "trakt_watchlist", "trakt_collection", "trakt_trending", "trakt_popular", "trakt_boxoffice",
@ -178,7 +178,7 @@ custom_sort_builders = [
"trakt_recommended_personal", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", "trakt_recommended_personal", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all",
"trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all",
"tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list", "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list",
"anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_userlist",
"mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special",
"mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
] ]
@ -600,7 +600,7 @@ class CollectionBuilder:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for album collections") raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for album collections")
elif not self.library.is_music and method_name in music_only_builders: elif not self.library.is_music and method_name in music_only_builders:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for music libraries") raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for music libraries")
elif self.collection_level != "episode" and method_name in episode_parts_only: elif not self.playlist and self.collection_level != "episode" and method_name in episode_parts_only:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed with Collection Level: episode") raise Failed(f"{self.Type} Error: {method_final} attribute only allowed with Collection Level: episode")
elif self.parts_collection and method_name not in parts_collection_valid: elif self.parts_collection and method_name not in parts_collection_valid:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed with Collection Level: {self.collection_level.capitalize()}") raise Failed(f"{self.Type} Error: {method_final} attribute not allowed with Collection Level: {self.collection_level.capitalize()}")
@ -934,7 +934,7 @@ class CollectionBuilder:
if method_name == "anidb_popular": if method_name == "anidb_popular":
self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=30, maximum=30))) self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=30, maximum=30)))
elif method_name in ["anidb_id", "anidb_relation"]: elif method_name in ["anidb_id", "anidb_relation"]:
for anidb_id in self.config.AniDB.validate_anidb_ids(method_data, self.language): for anidb_id in self.config.AniDB.validate_anidb_ids(method_data):
self.builders.append((method_name, anidb_id)) self.builders.append((method_name, anidb_id))
elif method_name == "anidb_tag": elif method_name == "anidb_tag":
for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"):
@ -955,6 +955,14 @@ class CollectionBuilder:
self.builders.append((method_name, anilist_id)) self.builders.append((method_name, anilist_id))
elif method_name in ["anilist_popular", "anilist_trending", "anilist_top_rated"]: elif method_name in ["anilist_popular", "anilist_trending", "anilist_top_rated"]:
self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=10))) self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=10)))
elif method_name == "anilist_userlist":
for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"):
dict_methods = {dm.lower(): dm for dm in dict_data}
self.builders.append((method_name, self.config.AniList.validate_userlist({
"username": util.parse(self.Type, "username", dict_data, methods=dict_methods, parent=method_name),
"list_name": util.parse(self.Type, "list_name", dict_data, methods=dict_methods, parent=method_name),
"sort_by": util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=anilist.userlist_sort_options),
})))
elif method_name == "anilist_search": elif method_name == "anilist_search":
if self.current_time.month in [12, 1, 2]: current_season = "winter" if self.current_time.month in [12, 1, 2]: current_season = "winter"
elif self.current_time.month in [3, 4, 5]: current_season = "spring" elif self.current_time.month in [3, 4, 5]: current_season = "spring"
@ -1318,7 +1326,7 @@ class CollectionBuilder:
elif "tautulli" in method: elif "tautulli" in method:
ids = self.library.Tautulli.get_rating_keys(self.library, value, self.playlist) ids = self.library.Tautulli.get_rating_keys(self.library, value, self.playlist)
elif "anidb" in method: elif "anidb" in method:
anidb_ids = self.config.AniDB.get_anidb_ids(method, value, self.language) anidb_ids = self.config.AniDB.get_anidb_ids(method, value)
ids = self.config.Convert.anidb_to_ids(anidb_ids, self.library) ids = self.config.Convert.anidb_to_ids(anidb_ids, self.library)
elif "anilist" in method: elif "anilist" in method:
anilist_ids = self.config.AniList.get_anilist_ids(method, value) anilist_ids = self.config.AniList.get_anilist_ids(method, value)
@ -1614,7 +1622,7 @@ class CollectionBuilder:
error = f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries" error = f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries"
elif self.library.is_movie and final_attr in plex.show_only_searches: elif self.library.is_movie and final_attr in plex.show_only_searches:
error = f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries" error = f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries"
elif self.library.is_music and final_attr not in plex.music_searches: elif self.library.is_music and final_attr not in plex.music_searches + ["all", "any"]:
error = f"{self.Type} Error: {final_attr} {method} attribute does not work for music libraries" error = f"{self.Type} Error: {final_attr} {method} attribute does not work for music libraries"
elif not self.library.is_music and final_attr in plex.music_searches: elif not self.library.is_music and final_attr in plex.music_searches:
error = f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries" error = f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries"
@ -1898,9 +1906,9 @@ class CollectionBuilder:
try: try:
if item is None: if item is None:
if is_movie: if is_movie:
item = self.config.TMDb.get_movie(item_id, partial="keywords") item = self.config.TMDb.get_movie(item_id)
else: else:
item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id), partial="keywords") item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id))
if check_released: if check_released:
date_to_check = item.release_date if is_movie else item.first_air_date date_to_check = item.release_date if is_movie else item.first_air_date
if not date_to_check or date_to_check > self.current_time: if not date_to_check or date_to_check > self.current_time:
@ -1913,7 +1921,7 @@ class CollectionBuilder:
elif filter_attr == "tmdb_type": elif filter_attr == "tmdb_type":
check_value = discover_types[item.type] check_value = discover_types[item.type]
elif filter_attr == "original_language": elif filter_attr == "original_language":
check_value = item.original_language.iso_639_1 check_value = item.language_iso
else: else:
raise Failed raise Failed
if (modifier == ".not" and check_value in filter_data) or (modifier == "" and check_value not in filter_data): if (modifier == ".not" and check_value in filter_data) or (modifier == "" and check_value not in filter_data):
@ -1936,11 +1944,11 @@ class CollectionBuilder:
return False return False
elif filter_attr in ["tmdb_genre", "tmdb_keyword", "origin_country"]: elif filter_attr in ["tmdb_genre", "tmdb_keyword", "origin_country"]:
if filter_attr == "tmdb_genre": if filter_attr == "tmdb_genre":
attrs = [g.name for g in item.genres] attrs = item.genres
elif filter_attr == "tmdb_keyword": elif filter_attr == "tmdb_keyword":
attrs = [k.name for k in item.keywords] attrs = item.keywords
elif filter_attr == "origin_country": elif filter_attr == "origin_country":
attrs = [c.iso_3166_1 for c in item.origin_countries] attrs = [c.iso_3166_1 for c in item.countries]
else: else:
raise Failed raise Failed
if (not list(set(filter_data) & set(attrs)) and modifier == "") \ if (not list(set(filter_data) & set(attrs)) and modifier == "") \
@ -2399,10 +2407,10 @@ class CollectionBuilder:
if "visible_library" in self.details and self.details["visible_library"] != visibility["library"]: if "visible_library" in self.details and self.details["visible_library"] != visibility["library"]:
visible_library = self.details["visible_library"] visible_library = self.details["visible_library"]
if "visible_home" in self.details and self.details["visible_home"] != visibility["library"]: if "visible_home" in self.details and self.details["visible_home"] != visibility["home"]:
visible_home = self.details["visible_home"] visible_home = self.details["visible_home"]
if "visible_shared" in self.details and self.details["visible_shared"] != visibility["library"]: if "visible_shared" in self.details and self.details["visible_shared"] != visibility["shared"]:
visible_shared = self.details["visible_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: if visible_library is not None or visible_home is not None or visible_shared is not None:

@ -22,7 +22,9 @@ class Cache:
cursor.execute("DROP TABLE IF EXISTS imdb_to_tvdb_map") cursor.execute("DROP TABLE IF EXISTS imdb_to_tvdb_map")
cursor.execute("DROP TABLE IF EXISTS tmdb_to_tvdb_map") cursor.execute("DROP TABLE IF EXISTS tmdb_to_tvdb_map")
cursor.execute("DROP TABLE IF EXISTS imdb_map") cursor.execute("DROP TABLE IF EXISTS imdb_map")
cursor.execute("DROP TABLE IF EXISTS mdb_data")
cursor.execute("DROP TABLE IF EXISTS omdb_data") cursor.execute("DROP TABLE IF EXISTS omdb_data")
cursor.execute("DROP TABLE IF EXISTS omdb_data2")
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids_map ( """CREATE TABLE IF NOT EXISTS guids_map (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
@ -70,11 +72,12 @@ class Cache:
expiration_date TEXT)""" expiration_date TEXT)"""
) )
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS omdb_data2 ( """CREATE TABLE IF NOT EXISTS omdb_data3 (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
imdb_id TEXT UNIQUE, imdb_id TEXT UNIQUE,
title TEXT, title TEXT,
year INTEGER, year INTEGER,
released TEXT,
content_rating TEXT, content_rating TEXT,
genres TEXT, genres TEXT,
imdb_rating REAL, imdb_rating REAL,
@ -87,11 +90,12 @@ class Cache:
expiration_date TEXT)""" expiration_date TEXT)"""
) )
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS mdb_data ( """CREATE TABLE IF NOT EXISTS mdb_data2 (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
key_id TEXT UNIQUE, key_id TEXT UNIQUE,
title TEXT, title TEXT,
year INTEGER, year INTEGER,
released TEXT,
type TEXT, type TEXT,
imdbid TEXT, imdbid TEXT,
traktid INTEGER, traktid INTEGER,
@ -109,6 +113,56 @@ class Cache:
certification TEXT, certification TEXT,
expiration_date TEXT)""" expiration_date TEXT)"""
) )
cursor.execute(
"""CREATE TABLE IF NOT EXISTS tmdb_movie_data (
key INTEGER PRIMARY KEY,
tmdb_id INTEGER UNIQUE,
title TEXT,
original_title TEXT,
studio TEXT,
overview TEXT,
tagline TEXT,
imdb_id TEXT,
poster_url TEXT,
backdrop_url TEXT,
vote_count INTEGER,
vote_average REAL,
language_iso TEXT,
language_name TEXT,
genres TEXT,
keywords TEXT,
release_date TEXT,
collection_id INTEGER,
collection_name TEXT,
expiration_date TEXT)"""
)
cursor.execute(
"""CREATE TABLE IF NOT EXISTS tmdb_show_data (
key INTEGER PRIMARY KEY,
tmdb_id INTEGER UNIQUE,
title TEXT,
original_title TEXT,
studio TEXT,
overview TEXT,
tagline TEXT,
imdb_id TEXT,
poster_url TEXT,
backdrop_url TEXT,
vote_count INTEGER,
vote_average REAL,
language_iso TEXT,
language_name TEXT,
genres TEXT,
keywords TEXT,
first_air_date TEXT,
last_air_date TEXT,
status TEXT,
type TEXT,
tvdb_id INTEGER,
countries TEXT,
seasons TEXT,
expiration_date TEXT)"""
)
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS anime_map ( """CREATE TABLE IF NOT EXISTS anime_map (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
@ -149,6 +203,17 @@ class Cache:
media_id TEXT, media_id TEXT,
media_type TEXT)""" media_type TEXT)"""
) )
cursor.execute(
"""CREATE TABLE IF NOT EXISTS imdb_parental (
key INTEGER PRIMARY KEY,
imdb_id TEXT,
nudity TEXT,
violence TEXT,
profanity TEXT,
alcohol TEXT,
frightening TEXT,
expiration_date TEXT)"""
)
cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'") cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'")
if cursor.fetchone()[0] > 0: if cursor.fetchone()[0] > 0:
cursor.execute(f"SELECT DISTINCT library FROM image_map") cursor.execute(f"SELECT DISTINCT library FROM image_map")
@ -267,7 +332,7 @@ class Cache:
sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ? WHERE {val1_name} = ?" sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ? WHERE {val1_name} = ?"
cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), val1)) cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), val1))
else: else:
sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ?{'' if media_type is None else ', media_type = ?'} WHERE {val1_name} = ?" sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ?, media_type = ? WHERE {val1_name} = ?"
cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), media_type, val1)) cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), media_type, val1))
def query_omdb(self, imdb_id, expiration): def query_omdb(self, imdb_id, expiration):
@ -276,12 +341,13 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM omdb_data2 WHERE imdb_id = ?", (imdb_id,)) cursor.execute("SELECT * FROM omdb_data3 WHERE imdb_id = ?", (imdb_id,))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
omdb_dict["imdbID"] = row["imdb_id"] if row["imdb_id"] else None omdb_dict["imdbID"] = row["imdb_id"] if row["imdb_id"] else None
omdb_dict["Title"] = row["title"] if row["title"] else None omdb_dict["Title"] = row["title"] if row["title"] else None
omdb_dict["Year"] = row["year"] if row["year"] else None omdb_dict["Year"] = row["year"] if row["year"] else None
omdb_dict["Released"] = row["released"] if row["released"] else None
omdb_dict["Rated"] = row["content_rating"] if row["content_rating"] else None omdb_dict["Rated"] = row["content_rating"] if row["content_rating"] else None
omdb_dict["Genre"] = row["genres"] if row["genres"] else None omdb_dict["Genre"] = row["genres"] if row["genres"] else None
omdb_dict["imdbRating"] = row["imdb_rating"] if row["imdb_rating"] else None omdb_dict["imdbRating"] = row["imdb_rating"] if row["imdb_rating"] else None
@ -302,14 +368,14 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("INSERT OR IGNORE INTO omdb_data2(imdb_id) VALUES(?)", (omdb.imdb_id,)) cursor.execute("INSERT OR IGNORE INTO omdb_data3(imdb_id) VALUES(?)", (omdb.imdb_id,))
update_sql = "UPDATE omdb_data2 SET title = ?, year = ?, content_rating = ?, genres = ?, " \ update_sql = "UPDATE omdb_data3 SET title = ?, year = ?, released = ?, content_rating = ?, genres = ?, " \
"imdb_rating = ?, imdb_votes = ?, metacritic_rating = ?, type = ?, series_id = ?, " \ "imdb_rating = ?, imdb_votes = ?, metacritic_rating = ?, type = ?, series_id = ?, " \
"season_num = ?, episode_num = ?, expiration_date = ? WHERE imdb_id = ?" "season_num = ?, episode_num = ?, expiration_date = ? WHERE imdb_id = ?"
cursor.execute(update_sql, (omdb.title, omdb.year, omdb.content_rating, omdb.genres_str, cursor.execute(update_sql, (
omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, omdb.title, omdb.year, omdb.released.strftime("%d %b %Y"), omdb.content_rating, omdb.genres_str,
omdb.series_id, omdb.season_num, omdb.episode_num, omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, omdb.series_id, omdb.season_num,
expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id)) omdb.episode_num, expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id))
def query_mdb(self, key_id, expiration): def query_mdb(self, key_id, expiration):
mdb_dict = {} mdb_dict = {}
@ -317,11 +383,12 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM mdb_data WHERE key_id = ?", (key_id,)) cursor.execute("SELECT * FROM mdb_data2 WHERE key_id = ?", (key_id,))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
mdb_dict["title"] = row["title"] if row["title"] else None mdb_dict["title"] = row["title"] if row["title"] else None
mdb_dict["year"] = row["year"] if row["year"] else None mdb_dict["year"] = row["year"] if row["year"] else None
mdb_dict["released"] = row["released"] if row["released"] else None
mdb_dict["type"] = row["type"] if row["type"] else None mdb_dict["type"] = row["type"] if row["type"] else None
mdb_dict["imdbid"] = row["imdbid"] if row["imdbid"] else None mdb_dict["imdbid"] = row["imdbid"] if row["imdbid"] else None
mdb_dict["traktid"] = row["traktid"] if row["traktid"] else None mdb_dict["traktid"] = row["traktid"] if row["traktid"] else None
@ -349,16 +416,118 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("INSERT OR IGNORE INTO mdb_data(key_id) VALUES(?)", (key_id,)) cursor.execute("INSERT OR IGNORE INTO mdb_data2(key_id) VALUES(?)", (key_id,))
update_sql = "UPDATE mdb_data SET title = ?, year = ?, type = ?, imdbid = ?, traktid = ?, " \ update_sql = "UPDATE mdb_data2 SET title = ?, year = ?, released = ?, type = ?, imdbid = ?, traktid = ?, " \
"tmdbid = ?, score = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \ "tmdbid = ?, score = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \
"trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \ "trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \
"letterboxd_rating = ?, certification = ?, commonsense = ?, expiration_date = ? WHERE key_id = ?" "letterboxd_rating = ?, certification = ?, commonsense = ?, expiration_date = ? WHERE key_id = ?"
cursor.execute(update_sql, ( cursor.execute(update_sql, (
mdb.title, mdb.year, mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid, mdb.score, mdb.imdb_rating, mdb.title, mdb.year, mdb.released.strftime("%Y-%m-%d"), mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid,
mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating, mdb.tomatoes_rating, mdb.score, mdb.imdb_rating, mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating,
mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating, mdb.content_rating, mdb.tomatoes_rating, mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating,
mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id mdb.content_rating, mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id
))
def query_tmdb_movie(self, tmdb_id, expiration):
tmdb_dict = {}
expired = None
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
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
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)))
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 = ?"
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
))
def query_tmdb_show(self, tmdb_id, expiration):
tmdb_dict = {}
expired = None
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM tmdb_show_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["first_air_date"] = datetime.strptime(row["first_air_date"], "%Y-%m-%d") if row["first_air_date"] else None
tmdb_dict["last_air_date"] = datetime.strptime(row["last_air_date"], "%Y-%m-%d") if row["last_air_date"] else None
tmdb_dict["status"] = row["status"] if row["status"] else None
tmdb_dict["type"] = row["type"] if row["type"] else None
tmdb_dict["tvdb_id"] = row["tvdb_id"] if row["tvdb_id"] else None
tmdb_dict["countries"] = row["countries"] if row["countries"] else ""
tmdb_dict["seasons"] = row["seasons"] if row["seasons"] else ""
datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d")
time_between_insertion = datetime.now() - datetime_object
expired = time_between_insertion.days > expiration
return tmdb_dict, expired
def update_tmdb_show(self, expired, obj, 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_show_data(tmdb_id) VALUES(?)", (obj.tmdb_id,))
update_sql = "UPDATE tmdb_show_data SET title = ?, original_title = ?, studio = ?, overview = ?, tagline = ?, imdb_id = ?, " \
"poster_url = ?, backdrop_url = ?, vote_count = ?, vote_average = ?, language_iso = ?, " \
"language_name = ?, genres = ?, keywords = ?, first_air_date = ?, last_air_date = ?, status = ?, " \
"type = ?, tvdb_id = ?, countries = ?, seasons = ?, expiration_date = ? WHERE tmdb_id = ?"
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.first_air_date.strftime("%Y-%m-%d") if obj.first_air_date else None,
obj.last_air_date.strftime("%Y-%m-%d") if obj.last_air_date else None,
obj.status, obj.type, obj.tvdb_id, "|".join([str(c) for c in obj.countries]), "|".join([str(s) for s in obj.seasons]),
expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id
)) ))
def query_anime_map(self, anime_id, id_type): def query_anime_map(self, anime_id, id_type):
@ -537,3 +706,33 @@ class Cache:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute(f"DELETE FROM list_ids WHERE list_key = ?", (list_key,)) cursor.execute(f"DELETE FROM list_ids WHERE list_key = ?", (list_key,))
def query_imdb_parental(self, imdb_id, expiration):
imdb_dict = {}
expired = None
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM imdb_parental WHERE imdb_id = ?", (imdb_id,))
row = cursor.fetchone()
if row:
imdb_dict["nudity"] = row["nudity"] if row["nudity"] else "None"
imdb_dict["violence"] = row["violence"] if row["violence"] else "None"
imdb_dict["profanity"] = row["profanity"] if row["profanity"] else "None"
imdb_dict["alcohol"] = row["alcohol"] if row["alcohol"] else "None"
imdb_dict["frightening"] = row["frightening"] if row["frightening"] 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
return imdb_dict, expired
def update_imdb_parental(self, expired, imdb_id, parental, 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 imdb_parental(imdb_id) VALUES(?)", (imdb_id,))
update_sql = "UPDATE imdb_parental SET nudity = ?, violence = ?, profanity = ?, alcohol = ?, " \
"frightening = ?, expiration_date = ? WHERE imdb_id = ?"
cursor.execute(update_sql, (parental["nudity"], parental["violence"], parental["profanity"], parental["alcohol"],
parental["frightening"], expiration_date.strftime("%Y-%m-%d"), imdb_id))

@ -31,8 +31,10 @@ from ruamel import yaml
logger = util.logger logger = util.logger
sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"} sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"}
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Tag Metadata"}
mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"} mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"}
mass_available_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Metadata"}
imdb_label_options = {"with_none": "Add IMDb Parental Labels including None", "without_none": "Add IMDb Parental Labels including None"}
mass_rating_options = { mass_rating_options = {
"tmdb": "Use TMDb Rating", "tmdb": "Use TMDb Rating",
"omdb": "Use IMDb Rating through OMDb", "omdb": "Use IMDb Rating through OMDb",
@ -44,7 +46,9 @@ mass_rating_options = {
"mdb_tomatoes": "Use Rotten Tomatoes Rating through MDbList", "mdb_tomatoes": "Use Rotten Tomatoes Rating through MDbList",
"mdb_tomatoesaudience": "Use Rotten Tomatoes Audience Rating through MDbList", "mdb_tomatoesaudience": "Use Rotten Tomatoes Audience Rating through MDbList",
"mdb_tmdb": "Use TMDb Rating through MDbList", "mdb_tmdb": "Use TMDb Rating through MDbList",
"mdb_letterboxd": "Use Letterboxd Rating through MDbList" "mdb_letterboxd": "Use Letterboxd Rating through MDbList",
"anidb_rating": "Use AniDB Rating",
"anidb_average": "Use AniDB Average"
} }
class ConfigFile: class ConfigFile:
@ -366,7 +370,8 @@ class ConfigFile:
logger.info("Connecting to TMDb...") logger.info("Connecting to TMDb...")
self.TMDb = TMDb(self, { self.TMDb = TMDb(self, {
"apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True),
"language": check_for_attribute(self.data, "language", parent="tmdb", default="en") "language": check_for_attribute(self.data, "language", parent="tmdb", default="en"),
"expiration": check_for_attribute(self.data, "cache_expiration", parent="tmdb", var_type="int", default=60)
}) })
logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}")
else: else:
@ -380,7 +385,7 @@ class ConfigFile:
try: try:
self.OMDb = OMDb(self, { self.OMDb = OMDb(self, {
"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True), "apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True),
"expiration": check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) "expiration": check_for_attribute(self.data, "cache_expiration", parent="omdb", var_type="int", default=60)
}) })
except Failed as e: except Failed as e:
self.errors.append(e) self.errors.append(e)
@ -397,7 +402,7 @@ class ConfigFile:
try: try:
self.Mdblist.add_key( self.Mdblist.add_key(
check_for_attribute(self.data, "apikey", parent="mdblist", throw=True), check_for_attribute(self.data, "apikey", parent="mdblist", throw=True),
check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) check_for_attribute(self.data, "cache_expiration", parent="mdblist", var_type="int", default=60)
) )
logger.info("Mdblist Connection Successful") logger.info("Mdblist Connection Successful")
except Failed as e: except Failed as e:
@ -445,7 +450,7 @@ class ConfigFile:
else: else:
logger.warning("mal attribute not found") logger.warning("mal attribute not found")
self.AniDB = AniDB(self) self.AniDB = AniDB(self, check_for_attribute(self.data, "language", parent="anidb", default="en"))
if "anidb" in self.data: if "anidb" in self.data:
logger.separator() logger.separator()
logger.info("Connecting to AniDB...") logger.info("Connecting to AniDB...")
@ -592,13 +597,16 @@ class ConfigFile:
"name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name), "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name),
"tmdb_collections": None, "tmdb_collections": None,
"genre_mapper": None, "genre_mapper": None,
"content_rating_mapper": None,
"radarr_remove_by_tag": None, "radarr_remove_by_tag": None,
"sonarr_remove_by_tag": None, "sonarr_remove_by_tag": None,
"mass_collection_mode": None, "mass_collection_mode": None,
"metadata_backup": None, "metadata_backup": None,
"genre_collections": None, "genre_collections": None,
"update_blank_track_titles": None, "update_blank_track_titles": None,
"mass_content_rating_update": None "mass_content_rating_update": None,
"mass_originally_available_update": None,
"mass_imdb_parental_labels": None
} }
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
@ -642,7 +650,7 @@ class ConfigFile:
params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True) params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True)
params["changes_webhooks"] = check_for_attribute(lib, "changes", parent="webhooks", var_type="list", default=self.webhooks["changes"], do_print=False, save=False, default_is_none=True) params["changes_webhooks"] = check_for_attribute(lib, "changes", parent="webhooks", var_type="list", default=self.webhooks["changes"], do_print=False, save=False, default_is_none=True)
params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False)
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False, do_print=False)
params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False)
params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False) params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False)
params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False) params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False)
@ -660,13 +668,17 @@ class ConfigFile:
if "delete_collections_with_less" in lib["operations"]: if "delete_collections_with_less" in lib["operations"]:
params["delete_collections_with_less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False) params["delete_collections_with_less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False)
if "mass_genre_update" in lib["operations"]: if "mass_genre_update" in lib["operations"]:
params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False) params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False)
if "mass_audience_rating_update" in lib["operations"]: if "mass_audience_rating_update" in lib["operations"]:
params["mass_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False) params["mass_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False)
if "mass_critic_rating_update" in lib["operations"]: if "mass_critic_rating_update" in lib["operations"]:
params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False) params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False)
if "mass_content_rating_update" in lib["operations"]: if "mass_content_rating_update" in lib["operations"]:
params["mass_content_rating_update"] = check_for_attribute(lib["operations"], "mass_content_rating_update", test_list=mass_content_options, default_is_none=True, save=False) params["mass_content_rating_update"] = check_for_attribute(lib["operations"], "mass_content_rating_update", test_list=mass_content_options, default_is_none=True, save=False)
if "mass_originally_available_update" in lib["operations"]:
params["mass_originally_available_update"] = check_for_attribute(lib["operations"], "mass_originally_available_update", test_list=mass_available_options, default_is_none=True, save=False)
if "mass_imdb_parental_labels" in lib["operations"]:
params["mass_imdb_parental_labels"] = check_for_attribute(lib["operations"], "mass_imdb_parental_labels", test_list=imdb_label_options, default_is_none=True, save=False)
if "mass_trakt_rating_update" in lib["operations"]: if "mass_trakt_rating_update" in lib["operations"]:
params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False) params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False)
if "split_duplicates" in lib["operations"]: if "split_duplicates" in lib["operations"]:
@ -722,18 +734,24 @@ class ConfigFile:
logger.error("Config Error: tmdb_collections blank using default settings") logger.error("Config Error: tmdb_collections blank using default settings")
if "genre_mapper" in lib["operations"]: if "genre_mapper" in lib["operations"]:
if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict): if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict):
params["genre_mapper"] = {} params["genre_mapper"] = lib["operations"]["genre_mapper"]
for new_genre, old_genres in lib["operations"]["genre_mapper"].items(): for old_genre, new_genre in lib["operations"]["genre_mapper"].items():
if old_genres is None: if old_genre == new_genre:
params["genre_mapper"][new_genre] = old_genres logger.error("Config Error: genres cannot be mapped to themselves")
else: else:
for old_genre in util.get_list(old_genres): params["genre_mapper"][old_genre] = new_genre if new_genre else None
if old_genre == new_genre:
logger.error("Config Error: genres cannot be mapped to themselves")
else:
params["genre_mapper"][old_genre] = new_genre
else: else:
logger.error("Config Error: genre_mapper is blank") logger.error("Config Error: genre_mapper is blank")
if "content_rating_mapper" in lib["operations"]:
if lib["operations"]["content_rating_mapper"] and isinstance(lib["operations"]["content_rating_mapper"], dict):
params["content_rating_mapper"] = lib["operations"]["content_rating_mapper"]
for old_content, new_content in lib["operations"]["content_rating_mapper"].items():
if old_content == new_content:
logger.error("Config Error: content rating cannot be mapped to themselves")
else:
params["content_rating_mapper"][old_content] = new_content if new_content else None
else:
logger.error("Config Error: content_rating_mapper is blank")
if "genre_collections" in lib["operations"]: if "genre_collections" in lib["operations"]:
params["genre_collections"] = { params["genre_collections"] = {
"exclude_genres": [], "exclude_genres": [],
@ -769,20 +787,12 @@ class ConfigFile:
self.errors.append(err) self.errors.append(err)
logger.error(err) logger.error(err)
if self.OMDb is None and params["mass_genre_update"] == "omdb": for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]:
error_check("mass_genre_update", "OMDb") if params[mass_key] == "omdb" and self.OMDb is None:
if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": error_check(mass_key, "OMDb")
error_check("mass_audience_rating_update", "OMDb") if params[mass_key] and params[mass_key].startswith("mdb") and not self.Mdblist.has_key:
if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": error_check(mass_key, "MdbList API")
error_check("mass_critic_rating_update", "OMDb")
if self.OMDb is None and params["mass_content_rating_update"] == "omdb":
error_check("mass_content_rating_update", "OMDb")
if not self.Mdblist.has_key and params["mass_audience_rating_update"] in util.mdb_types:
error_check("mass_audience_rating_update", "MdbList API")
if not self.Mdblist.has_key and params["mass_critic_rating_update"] in util.mdb_types:
error_check("mass_critic_rating_update", "MdbList API")
if not self.Mdblist.has_key and params["mass_content_rating_update"] in ["mdb", "mdb_commonsense"]:
error_check("mass_content_rating_update", "MdbList API")
if self.Trakt is None and params["mass_trakt_rating_update"]: if self.Trakt is None and params["mass_trakt_rating_update"]:
error_check("mass_trakt_rating_update", "Trakt") error_check("mass_trakt_rating_update", "Trakt")

@ -10,52 +10,39 @@ anime_lists_url = "https://raw.githubusercontent.com/Fribb/anime-lists/master/an
class Convert: class Convert:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self._loaded = False
self._anidb_ids = {} self._anidb_ids = {}
self._mal_to_anidb = {} self._mal_to_anidb = {}
self._anilist_to_anidb = {} self._anilist_to_anidb = {}
self._anidb_to_imdb = {} self._anidb_to_imdb = {}
self._anidb_to_tvdb = {} self._anidb_to_tvdb = {}
self._imdb_to_anidb = {}
self._tvdb_to_anidb = {}
for anime_id in self.config.get_json(anime_lists_url):
if "anidb_id" in anime_id:
self._anidb_ids[anime_id["anidb_id"]] = anime_id
if "mal_id" in anime_id:
self._mal_to_anidb[int(anime_id["mal_id"])] = int(anime_id["anidb_id"])
if "anilist_id" in anime_id:
self._anilist_to_anidb[int(anime_id["anilist_id"])] = int(anime_id["anidb_id"])
if "imdb_id" in anime_id and str(anime_id["imdb_id"]).startswith("tt"):
self._anidb_to_imdb[int(anime_id["anidb_id"])] = util.get_list(anime_id["imdb_id"])
for im_id in util.get_list(anime_id["imdb_id"]):
self._imdb_to_anidb[im_id] = int(anime_id["anidb_id"])
if "thetvdb_id" in anime_id:
self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"])
self._tvdb_to_anidb[int(anime_id["thetvdb_id"])] = int(anime_id["anidb_id"])
@property def imdb_to_anidb(self, imdb_id):
def anidb_ids(self): if imdb_id in self._imdb_to_anidb:
self._load_anime_conversion() return self._imdb_to_anidb[imdb_id]
return self._anidb_ids else:
raise Failed(f"AniDB ID not found for IMDb ID: {imdb_id}")
@property
def mal_to_anidb(self):
self._load_anime_conversion()
return self._mal_to_anidb
@property
def anilist_to_anidb(self):
self._load_anime_conversion()
return self._anilist_to_anidb
@property
def anidb_to_imdb(self):
self._load_anime_conversion()
return self._anidb_to_imdb
@property
def anidb_to_tvdb(self):
self._load_anime_conversion()
return self._anidb_to_tvdb
def _load_anime_conversion(self): def tvdb_to_anidb(self, tvdb_id):
if not self._loaded: if int(tvdb_id) in self._tvdb_to_anidb:
for anime_id in self.config.get_json(anime_lists_url): return self._tvdb_to_anidb[int(tvdb_id)]
if "anidb_id" in anime_id: else:
self._anidb_ids[anime_id["anidb_id"]] = anime_id raise Failed(f"AniDB ID not found for TVDb ID: {tvdb_id}")
if "mal_id" in anime_id:
self._mal_to_anidb[int(anime_id["mal_id"])] = int(anime_id["anidb_id"])
if "anilist_id" in anime_id:
self._anilist_to_anidb[int(anime_id["anilist_id"])] = int(anime_id["anidb_id"])
if "imdb_id" in anime_id and str(anime_id["imdb_id"]).startswith("tt"):
self._anidb_to_imdb[int(anime_id["anidb_id"])] = util.get_list(anime_id["imdb_id"])
if "thetvdb_id" in anime_id:
self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"])
self._loaded = True
def anidb_to_ids(self, anidb_ids, library): def anidb_to_ids(self, anidb_ids, library):
ids = [] ids = []
@ -63,18 +50,18 @@ class Convert:
for anidb_id in anidb_list: for anidb_id in anidb_list:
if anidb_id in library.anidb_map: if anidb_id in library.anidb_map:
ids.append((library.anidb_map[anidb_id], "ratingKey")) ids.append((library.anidb_map[anidb_id], "ratingKey"))
elif anidb_id in self.anidb_to_imdb: elif anidb_id in self._anidb_to_imdb:
added = False added = False
for imdb in self.anidb_to_imdb[anidb_id]: for imdb in self._anidb_to_imdb[anidb_id]:
tmdb, tmdb_type = self.imdb_to_tmdb(imdb) tmdb, tmdb_type = self.imdb_to_tmdb(imdb)
if tmdb and tmdb_type == "movie": if tmdb and tmdb_type == "movie":
ids.append((tmdb, "tmdb")) ids.append((tmdb, "tmdb"))
added = True added = True
if added is False and anidb_id in self.anidb_to_tvdb: if added is False and anidb_id in self._anidb_to_tvdb:
ids.append((self.anidb_to_tvdb[anidb_id], "tvdb")) ids.append((self._anidb_to_tvdb[anidb_id], "tvdb"))
elif anidb_id in self.anidb_to_tvdb: elif anidb_id in self._anidb_to_tvdb:
ids.append((self.anidb_to_tvdb[anidb_id], "tvdb")) ids.append((self._anidb_to_tvdb[anidb_id], "tvdb"))
elif anidb_id in self.anidb_ids: elif anidb_id in self._anidb_ids:
logger.warning(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}") logger.warning(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}")
else: else:
logger.warning(f"Convert Error: AniDB ID: {anidb_id} not found") logger.warning(f"Convert Error: AniDB ID: {anidb_id} not found")
@ -83,8 +70,8 @@ class Convert:
def anilist_to_ids(self, anilist_ids, library): def anilist_to_ids(self, anilist_ids, library):
anidb_ids = [] anidb_ids = []
for anilist_id in anilist_ids: for anilist_id in anilist_ids:
if anilist_id in self.anilist_to_anidb: if anilist_id in self._anilist_to_anidb:
anidb_ids.append(self.anilist_to_anidb[anilist_id]) anidb_ids.append(self._anilist_to_anidb[anilist_id])
else: else:
logger.warning(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}") logger.warning(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}")
return self.anidb_to_ids(anidb_ids, library) return self.anidb_to_ids(anidb_ids, library)
@ -94,8 +81,8 @@ class Convert:
for mal_id in mal_ids: for mal_id in mal_ids:
if int(mal_id) in library.mal_map: if int(mal_id) in library.mal_map:
ids.append((library.mal_map[int(mal_id)], "ratingKey")) ids.append((library.mal_map[int(mal_id)], "ratingKey"))
elif int(mal_id) in self.mal_to_anidb: elif int(mal_id) in self._mal_to_anidb:
ids.extend(self.anidb_to_ids(self.mal_to_anidb[int(mal_id)], library)) ids.extend(self.anidb_to_ids(self._mal_to_anidb[int(mal_id)], library))
else: else:
logger.warning(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}") logger.warning(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}")
return ids return ids
@ -275,26 +262,26 @@ class Convert:
raise Failed(f"Hama Agent ID: {check_id} not supported") raise Failed(f"Hama Agent ID: {check_id} not supported")
elif item_type == "myanimelist": elif item_type == "myanimelist":
library.mal_map[int(check_id)] = item.ratingKey library.mal_map[int(check_id)] = item.ratingKey
if int(check_id) in self.mal_to_anidb: if int(check_id) in self._mal_to_anidb:
anidb_id = self.mal_to_anidb[int(check_id)] anidb_id = self._mal_to_anidb[int(check_id)]
else: else:
raise Failed(f"AniDB ID not found for MyAnimeList ID: {check_id}") raise Failed(f"AniDB ID not found for MyAnimeList ID: {check_id}")
elif item_type == "local": raise Failed("No match in Plex") elif item_type == "local": raise Failed("No match in Plex")
else: raise Failed(f"Agent {item_type} not supported") else: raise Failed(f"Agent {item_type} not supported")
if anidb_id: if anidb_id:
if anidb_id in self.anidb_to_imdb: if anidb_id in self._anidb_to_imdb:
added = False added = False
for imdb in self.anidb_to_imdb[anidb_id]: for imdb in self._anidb_to_imdb[anidb_id]:
tmdb, tmdb_type = self.imdb_to_tmdb(imdb) tmdb, tmdb_type = self.imdb_to_tmdb(imdb)
if tmdb and tmdb_type == "movie": if tmdb and tmdb_type == "movie":
imdb_id.append(imdb) imdb_id.append(imdb)
tmdb_id.append(tmdb) tmdb_id.append(tmdb)
added = True added = True
if added is False and anidb_id in self.anidb_to_tvdb: if added is False and anidb_id in self._anidb_to_tvdb:
tvdb_id.append(self.anidb_to_tvdb[anidb_id]) tvdb_id.append(self._anidb_to_tvdb[anidb_id])
elif anidb_id in self.anidb_to_tvdb: elif anidb_id in self._anidb_to_tvdb:
tvdb_id.append(self.anidb_to_tvdb[anidb_id]) tvdb_id.append(self._anidb_to_tvdb[anidb_id])
else: else:
raise Failed(f"AniDB: {anidb_id} not found") raise Failed(f"AniDB: {anidb_id} not found")
else: else:

@ -128,6 +128,24 @@ class IMDb:
return imdb_ids return imdb_ids
raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}") raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}")
def parental_guide(self, imdb_id, ignore_cache=False):
parental_dict = {}
expired = None
if self.config.Cache and not ignore_cache:
parental_dict, expired = self.config.Cache.query_imdb_parental(imdb_id, self.config.expiration)
if parental_dict and expired is False:
return parental_dict
response = self.config.get_html(f"https://www.imdb.com/title/{imdb_id}/parentalguide")
for ptype in ["nudity", "violence", "profanity", "alcohol", "frightening"]:
results = response.xpath(f"//section[@id='advisory-{ptype}']//span[contains(@class,'ipl-status-pill')]/text()")
if results:
parental_dict[ptype] = results[0].strip()
else:
raise Failed(f"IMDb Error: No Item Found for IMDb ID: {imdb_id}")
if self.config.Cache and not ignore_cache:
self.config.Cache.update_imdb_parental(expired, imdb_id, parental_dict, self.config.expiration)
return parental_dict
def _ids_from_chart(self, chart): def _ids_from_chart(self, chart):
if chart == "box_office": if chart == "box_office":
url = "chart/boxoffice" url = "chart/boxoffice"

@ -72,6 +72,8 @@ class Library(ABC):
self.mass_audience_rating_update = params["mass_audience_rating_update"] self.mass_audience_rating_update = params["mass_audience_rating_update"]
self.mass_critic_rating_update = params["mass_critic_rating_update"] self.mass_critic_rating_update = params["mass_critic_rating_update"]
self.mass_content_rating_update = params["mass_content_rating_update"] self.mass_content_rating_update = params["mass_content_rating_update"]
self.mass_originally_available_update = params["mass_originally_available_update"]
self.mass_imdb_parental_labels = params["mass_imdb_parental_labels"]
self.mass_trakt_rating_update = params["mass_trakt_rating_update"] self.mass_trakt_rating_update = params["mass_trakt_rating_update"]
self.radarr_add_all_existing = params["radarr_add_all_existing"] self.radarr_add_all_existing = params["radarr_add_all_existing"]
self.radarr_remove_by_tag = params["radarr_remove_by_tag"] self.radarr_remove_by_tag = params["radarr_remove_by_tag"]
@ -83,6 +85,7 @@ class Library(ABC):
self.tmdb_collections = params["tmdb_collections"] self.tmdb_collections = params["tmdb_collections"]
self.genre_collections = params["genre_collections"] self.genre_collections = params["genre_collections"]
self.genre_mapper = params["genre_mapper"] self.genre_mapper = params["genre_mapper"]
self.content_rating_mapper = params["content_rating_mapper"]
self.error_webhooks = params["error_webhooks"] self.error_webhooks = params["error_webhooks"]
self.changes_webhooks = params["changes_webhooks"] self.changes_webhooks = params["changes_webhooks"]
self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex?
@ -93,8 +96,8 @@ class Library(ABC):
self.status = {} self.status = {}
self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \
or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_trakt_rating_update \ or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_imdb_parental_labels or self.mass_trakt_rating_update \
or self.genre_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False or self.genre_mapper or self.content_rating_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False
self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \
or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \
or self.genre_collections or self.show_unmanaged or self.metadata_backup or self.update_blank_track_titles else False or self.genre_collections or self.show_unmanaged or self.metadata_backup or self.update_blank_track_titles else False

@ -1,3 +1,4 @@
from datetime import datetime
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
from urllib.parse import urlparse from urllib.parse import urlparse
@ -17,6 +18,10 @@ class MDbObj:
self._data = data self._data = data
self.title = data["title"] self.title = data["title"]
self.year = util.check_num(data["year"]) self.year = util.check_num(data["year"])
try:
self.released = datetime.strptime(data["released"], "%Y-%m-%d")
except ValueError:
self.released = None
self.type = data["type"] self.type = data["type"]
self.imdbid = data["imdbid"] self.imdbid = data["imdbid"]
self.traktid = util.check_num(data["traktid"]) self.traktid = util.check_num(data["traktid"])

@ -246,9 +246,11 @@ class MetadataFile(DataFile):
all_items = None all_items = None
if self.dynamic_collections: if self.dynamic_collections:
logger.info("") logger.info("")
logger.separator(f"Dynamic Collections") logger.separator("Dynamic Collections")
logger.info("")
for map_name, dynamic in self.dynamic_collections.items(): for map_name, dynamic in self.dynamic_collections.items():
logger.info("")
logger.separator(f"Building {map_name} Dynamic Collections", space=False, border=False)
logger.info("")
try: try:
methods = {dm.lower(): dm for dm in dynamic} methods = {dm.lower(): dm for dm in dynamic}
if "type" not in methods: if "type" not in methods:
@ -268,11 +270,11 @@ class MetadataFile(DataFile):
if og_exclude and include: if og_exclude and include:
raise Failed(f"Config Error: {map_name} cannot have both include and exclude attributes") raise Failed(f"Config Error: {map_name} cannot have both include and exclude attributes")
addons = util.parse("Config", "addons", dynamic, parent=map_name, methods=methods, datatype="dictlist") if "addons" in methods else {} addons = util.parse("Config", "addons", dynamic, parent=map_name, methods=methods, datatype="dictlist") if "addons" in methods else {}
exclude = [e for e in og_exclude] exclude = [str(e) for e in og_exclude]
for k, v in addons.items(): for k, v in addons.items():
if k in v: if k in v:
logger.warning(f"Config Warning: {k} cannot be an addon for itself") logger.warning(f"Config Warning: {k} cannot be an addon for itself")
exclude.extend([vv for vv in v if vv != k]) exclude.extend([str(vv) for vv in v if str(vv) != str(k)])
default_title_format = "<<key_name>>" default_title_format = "<<key_name>>"
default_template = None default_template = None
auto_list = {} auto_list = {}
@ -283,10 +285,10 @@ class MetadataFile(DataFile):
auto_list[ck] = cv auto_list[ck] = cv
if auto_type in ["genre", "mood", "style", "country", "network", "year", "decade", "content_rating", "subtitle_language", "audio_language", "resolution"]: if auto_type in ["genre", "mood", "style", "country", "network", "year", "decade", "content_rating", "subtitle_language", "audio_language", "resolution"]:
search_tag = auto_type_translation[auto_type] if auto_type in auto_type_translation else auto_type search_tag = auto_type_translation[auto_type] if auto_type in auto_type_translation else auto_type
if auto_type in ["subtitle_language", "audio_language"]: if auto_type in ["decade", "subtitle_language", "audio_language"]:
auto_list = {i.key: i.title for i in library.get_tags(search_tag) if i.title not in exclude and i.key not in exclude} auto_list = {str(i.key): i.title for i in library.get_tags(search_tag) if str(i.title) not in exclude and str(i.key) not in exclude}
else: else:
auto_list = {i.title: i.title for i in library.get_tags(search_tag) if i.title not in exclude} auto_list = {str(i.title): i.title for i in library.get_tags(search_tag) if str(i.title) not in exclude}
if library.is_music: if library.is_music:
default_template = {"smart_filter": {"limit": 50, "sort_by": "plays.desc", "any": {f"artist_{auto_type}": f"<<{auto_type}>>"}}} default_template = {"smart_filter": {"limit": 50, "sort_by": "plays.desc", "any": {f"artist_{auto_type}": f"<<{auto_type}>>"}}}
default_title_format = "Most Played <<key_name>> <<library_type>>s" default_title_format = "Most Played <<key_name>> <<library_type>>s"
@ -303,8 +305,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_id, tvdb_id, imdb_id = library.get_ids(item)
tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=True) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=True)
if tmdb_item and tmdb_item.collection and tmdb_item.collection.id not in exclude and tmdb_item.collection.name not in exclude: if tmdb_item and tmdb_item.collection_id and tmdb_item.collection_id not in exclude and tmdb_item.collection_name not in exclude:
auto_list[tmdb_item.collection.id] = tmdb_item.collection.name auto_list[str(tmdb_item.collection_id)] = tmdb_item.collection_name
logger.exorcise() logger.exorcise()
elif auto_type == "original_language": elif auto_type == "original_language":
if not all_items: if not all_items:
@ -313,8 +315,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_id, tvdb_id, imdb_id = library.get_ids(item)
tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie") tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie")
if tmdb_item and tmdb_item.original_language and tmdb_item.original_language.iso_639_1 not in exclude and tmdb_item.original_language.english_name not in exclude: if tmdb_item and tmdb_item.language_iso and tmdb_item.language_iso not in exclude and tmdb_item.language_name not in exclude:
auto_list[tmdb_item.original_language.iso_639_1] = tmdb_item.original_language.english_name auto_list[tmdb_item.language_iso] = tmdb_item.language_name
logger.exorcise() logger.exorcise()
default_title_format = "<<key_name>> <<library_type>>s" default_title_format = "<<key_name>> <<library_type>>s"
elif auto_type == "origin_country": elif auto_type == "origin_country":
@ -324,8 +326,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_id, tvdb_id, imdb_id = library.get_ids(item)
tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie") tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie")
if tmdb_item and tmdb_item.origin_countries: if tmdb_item and tmdb_item.countries:
for country in tmdb_item.origin_countries: for country in tmdb_item.countries:
if country.iso_3166_1 not in exclude and country.name not in exclude: if country.iso_3166_1 not in exclude and country.name not in exclude:
auto_list[country.iso_3166_1] = country.name auto_list[country.iso_3166_1] = country.name
logger.exorcise() logger.exorcise()
@ -369,7 +371,7 @@ class MetadataFile(DataFile):
try: try:
results = self.config.TMDb.search_people(role["name"]) results = self.config.TMDb.search_people(role["name"])
if results[0].id not in exclude: if results[0].id not in exclude:
auto_list[results[0].id] = results[0].name auto_list[str(results[0].id)] = results[0].name
person_count += 1 person_count += 1
except TMDbNotFound: except TMDbNotFound:
logger.error(f"TMDb Error: Actor {role['name']} Not Found") logger.error(f"TMDb Error: Actor {role['name']} Not Found")
@ -399,8 +401,8 @@ class MetadataFile(DataFile):
methods["title_override"] = methods.pop("post_format_override") methods["title_override"] = methods.pop("post_format_override")
if "pre_format_override" in methods: if "pre_format_override" in methods:
methods["key_name_override"] = methods.pop("pre_format_override") methods["key_name_override"] = methods.pop("pre_format_override")
title_override = util.parse("Config", "title_override", dynamic, parent=map_name, methods=methods, datatype="dict") if "title_override" in methods else {} title_override = util.parse("Config", "title_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "title_override" in methods else {}
key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="dict") if "key_name_override" in methods else {} key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "key_name_override" in methods else {}
test = util.parse("Config", "test", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "test" in methods else False test = util.parse("Config", "test", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "test" in methods else False
sync = util.parse("Config", "sync", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "sync" in methods else False sync = util.parse("Config", "sync", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "sync" in methods else False
if "<<library_type>>" in title_format: if "<<library_type>>" in title_format:
@ -445,23 +447,28 @@ class MetadataFile(DataFile):
if key not in exclude: if key not in exclude:
other_keys.append(key) other_keys.append(key)
continue continue
template_call = {"name": template_name, auto_type: [key] + addons[key] if key in addons else key} if key in key_name_override:
key_name = key_name_override[key]
else:
key_name = value
for prefix in remove_prefix:
if key_name.startswith(prefix):
key_name = key_name[len(prefix):].strip()
for suffix in remove_suffix:
if key_name.endswith(suffix):
key_name = key_name[:-len(suffix)].strip()
template_call = {
"name": template_name,
auto_type: [key] + addons[key] if key in addons else [key],
"key_name": key_name, "key": key
}
for k, v in template_variables.items(): for k, v in template_variables.items():
if key in v: if key in v:
template_call[k] = v[key] template_call[k] = v[key]
if key in title_override: if key in title_override:
collection_title = title_override[key] collection_title = title_override[key]
else: else:
if key in key_name_override: collection_title = title_format.replace("<<title>>", key_name).replace("<<key_name>>", key_name)
value = key_name_override[key]
else:
for prefix in remove_prefix:
if value.startswith(prefix):
value = value[len(prefix):].strip()
for suffix in remove_suffix:
if value.endswith(suffix):
value = value[:-len(suffix)].strip()
collection_title = title_format.replace("<<title>>", value).replace("<<key_name>>", value)
if collection_title in col_names: if collection_title in col_names:
logger.warning(f"Config Warning: Skipping duplicate collection: {collection_title}") logger.warning(f"Config Warning: Skipping duplicate collection: {collection_title}")
else: else:
@ -662,18 +669,14 @@ class MetadataFile(DataFile):
genres = [] genres = []
if tmdb_item: if tmdb_item:
originally_available = datetime.strftime(tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date, "%Y-%m-%d") originally_available = datetime.strftime(tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date, "%Y-%m-%d")
if tmdb_is_movie and tmdb_item.original_title != tmdb_item.title:
if tmdb_item.original_title != tmdb_item.title:
original_title = tmdb_item.original_title original_title = tmdb_item.original_title
elif not tmdb_is_movie and tmdb_item.original_name != tmdb_item.name:
original_title = tmdb_item.original_name
rating = tmdb_item.vote_average rating = tmdb_item.vote_average
if tmdb_is_movie and tmdb_item.companies: studio = tmdb_item.studio
studio = tmdb_item.companies[0].name
elif not tmdb_is_movie and tmdb_item.networks:
studio = tmdb_item.networks[0].name
tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None
summary = tmdb_item.overview summary = tmdb_item.overview
genres = [genre.name for genre in tmdb_item.genres] genres = tmdb_item.genres
edits = {} edits = {}
add_edit("title", item, meta, methods, value=title) add_edit("title", item, meta, methods, value=title)

@ -1,3 +1,4 @@
from datetime import datetime
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
@ -11,40 +12,33 @@ class OMDbObj:
self._data = data self._data = data
if data["Response"] == "False": if data["Response"] == "False":
raise Failed(f"OMDb Error: {data['Error']} IMDb ID: {imdb_id}") raise Failed(f"OMDb Error: {data['Error']} IMDb ID: {imdb_id}")
self.title = data["Title"] def _parse(key, is_int=False, is_float=False, is_date=False, replace=None):
try: try:
self.year = int(data["Year"]) value = str(data[key]).replace(replace, '') if replace else data[key]
except (ValueError, TypeError): if is_int:
self.year = None return int(value)
self.content_rating = data["Rated"] elif is_float:
self.genres = util.get_list(data["Genre"]) return float(value)
self.genres_str = data["Genre"] elif is_date:
try: return datetime.strptime(value, "%d %b %Y")
self.imdb_rating = float(data["imdbRating"]) else:
except (ValueError, TypeError): return value
self.imdb_rating = None except (ValueError, TypeError, KeyError):
try: return None
self.imdb_votes = int(str(data["imdbVotes"]).replace(',', '')) self.title = _parse("Title")
except (ValueError, TypeError): self.year = _parse("Year", is_int=True)
self.imdb_votes = None self.released = _parse("Released", is_date=True)
try: self.content_rating = _parse("Rated")
self.metacritic_rating = int(data["Metascore"]) self.genres_str = _parse("Genre")
except (ValueError, TypeError): self.genres = util.get_list(self.genres_str)
self.metacritic_rating = None self.imdb_rating = _parse("imdbRating", is_float=True)
self.imdb_id = data["imdbID"] self.imdb_votes = _parse("imdbVotes", is_int=True, replace=",")
self.type = data["Type"] self.metacritic_rating = _parse("Metascore", is_int=True)
try: self.imdb_id = _parse("imdbID")
self.series_id = data["seriesID"] self.type = _parse("Type")
except (ValueError, TypeError, KeyError): self.series_id = _parse("seriesID")
self.series_id = None self.season_num = _parse("Season", is_int=True)
try: self.episode_num = _parse("Episode", is_int=True)
self.season_num = int(data["Season"])
except (ValueError, TypeError, KeyError):
self.season_num = None
try:
self.episode_num = int(data["Episode"])
except (ValueError, TypeError, KeyError):
self.episode_num = None
class OMDb: class OMDb:

@ -793,6 +793,7 @@ class Plex(Library):
def get_filter_items(self, uri_args): def get_filter_items(self, uri_args):
key = f"/library/sections/{self.Plex.key}/all{uri_args}" key = f"/library/sections/{self.Plex.key}/all{uri_args}"
logger.debug(key)
return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE) return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE)
def get_collection_name_and_items(self, collection, smart_label_collection): def get_collection_name_and_items(self, collection, smart_label_collection):

@ -110,7 +110,7 @@ class Sonarr:
invalid.extend(_i) invalid.extend(_i)
except ArrException as e: except ArrException as e:
logger.stacktrace() logger.stacktrace()
raise Failed(f"Radarr Error: {e}") raise Failed(f"Sonarr Error: {e}")
for i, item in enumerate(tvdb_ids, 1): for i, item in enumerate(tvdb_ids, 1):
path = item[1] if isinstance(item, tuple) else None path = item[1] if isinstance(item, tuple) else None

@ -57,11 +57,100 @@ discover_movie_sort = [
discover_tv_sort = ["vote_average.desc", "vote_average.asc", "first_air_date.desc", "first_air_date.asc", "popularity.desc", "popularity.asc"] discover_tv_sort = ["vote_average.desc", "vote_average.asc", "first_air_date.desc", "first_air_date.asc", "popularity.desc", "popularity.asc"]
discover_monetization_types = ["flatrate", "free", "ads", "rent", "buy"] discover_monetization_types = ["flatrate", "free", "ads", "rent", "buy"]
class TMDbCountry:
def __init__(self, data):
self.iso_3166_1 = data.split(":")[0] if isinstance(data, str) else data.iso_3166_1
self.name = data.split(":")[1] if isinstance(data, str) else data.name
def __repr__(self):
return f"{self.iso_3166_1}:{self.name}"
class TMDbSeason:
def __init__(self, data):
self.season_number = data.split(":")[0] if isinstance(data, str) else data.season_number
self.name = data.split(":")[1] if isinstance(data, str) else data.name
def __repr__(self):
return f"{self.season_number}:{self.name}"
class TMDBObj:
def __init__(self, tmdb, tmdb_id, ignore_cache=False):
self._tmdb = tmdb
self.tmdb_id = tmdb_id
self.ignore_cache = ignore_cache
def _load(self, data):
self.title = data["title"] if isinstance(data, dict) else data.title
self.tagline = data["tagline"] if isinstance(data, dict) else data.tagline
self.overview = data["overview"] if isinstance(data, dict) else data.overview
self.imdb_id = data["imdb_id"] if isinstance(data, dict) else data.imdb_id
self.poster_url = data["poster_url"] if isinstance(data, dict) else data.poster_url
self.backdrop_url = data["backdrop_url"] if isinstance(data, dict) else data.backdrop_url
self.vote_count = data["vote_count"] if isinstance(data, dict) else data.vote_count
self.vote_average = data["vote_average"] if isinstance(data, dict) else data.vote_average
self.language_iso = data["language_iso"] if isinstance(data, dict) else data.original_language.iso_639_1 if data.original_language else None
self.language_name = data["language_name"] if isinstance(data, dict) else data.original_language.english_name if data.original_language else None
self.genres = data["genres"].split("|") if isinstance(data, dict) else [g.name for g in data.genres]
self.keywords = data["keywords"].split("|") if isinstance(data, dict) else [g.name for g in data.keywords]
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
if self._tmdb.config.Cache and not ignore_cache:
data, expired = self._tmdb.config.Cache.query_tmdb_movie(tmdb_id, self._tmdb.expiration)
if expired or not data:
data = self._tmdb.TMDb.movie(self.tmdb_id, partial="external_ids,keywords")
super()._load(data)
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.config.Cache and not ignore_cache:
self._tmdb.config.Cache.update_tmdb_movie(expired, self, self._tmdb.expiration)
class TMDbShow(TMDBObj):
def __init__(self, tmdb, tmdb_id, ignore_cache=False):
super().__init__(tmdb, tmdb_id, ignore_cache=ignore_cache)
expired = None
data = None
if self._tmdb.config.Cache and not ignore_cache:
data, expired = self._tmdb.config.Cache.query_tmdb_show(tmdb_id, self._tmdb.expiration)
if expired or not data:
data = self._tmdb.TMDb.tv_show(self.tmdb_id, partial="external_ids,keywords")
super()._load(data)
self.original_title = data["original_title"] if isinstance(data, dict) else data.original_name
self.first_air_date = data["first_air_date"] if isinstance(data, dict) else data.first_air_date
self.last_air_date = data["last_air_date"] if isinstance(data, dict) else data.last_air_date
self.status = data["status"] if isinstance(data, dict) else data.status
self.type = data["type"] if isinstance(data, dict) else data.type
self.studio = data["studio"] if isinstance(data, dict) else data.networks[0].name if data.networks else None
self.tvdb_id = data["tvdb_id"] if isinstance(data, dict) else data.tvdb_id
loop = data["countries"].split("|") if isinstance(data, dict) else data.origin_countries
self.countries = [TMDbCountry(c) for c in loop]
loop = data["seasons"].split("|") if isinstance(data, dict) else data.seasons
self.seasons = [TMDbSeason(s) for s in loop]
if self._tmdb.config.Cache and not ignore_cache:
self._tmdb.config.Cache.update_tmdb_show(expired, self, self._tmdb.expiration)
class TMDb: class TMDb:
def __init__(self, config, params): def __init__(self, config, params):
self.config = config self.config = config
self.apikey = params["apikey"] self.apikey = params["apikey"]
self.language = params["language"] self.language = params["language"]
self.expiration = params["expiration"]
logger.secret(self.apikey) logger.secret(self.apikey)
try: try:
self.TMDb = TMDbAPIs(self.apikey, language=self.language, session=self.config.session) self.TMDb = TMDbAPIs(self.apikey, language=self.language, session=self.config.session)
@ -69,7 +158,7 @@ class TMDb:
raise Failed(f"TMDb Error: {e}") raise Failed(f"TMDb Error: {e}")
def convert_from(self, tmdb_id, convert_to, is_movie): def convert_from(self, tmdb_id, convert_to, is_movie):
item = self.get_movie(tmdb_id, partial="external_ids") if is_movie else self.get_show(tmdb_id, partial="external_ids") item = self.get_movie(tmdb_id) if is_movie else self.get_show(tmdb_id)
check_id = item.tvdb_id if convert_to == "tvdb_id" and not is_movie else item.imdb_id check_id = item.tvdb_id if convert_to == "tvdb_id" and not is_movie else item.imdb_id
if not check_id: if not check_id:
raise Failed(f"TMDb Error: No {convert_to.upper().replace('B_', 'b ')} found for TMDb ID {tmdb_id}") raise Failed(f"TMDb Error: No {convert_to.upper().replace('B_', 'b ')} found for TMDb ID {tmdb_id}")
@ -106,12 +195,12 @@ class TMDb:
except Failed: raise Failed(f"TMDb Error: No Movie or Collection found for TMDb ID {tmdb_id}") except Failed: raise Failed(f"TMDb Error: No Movie or Collection found for TMDb ID {tmdb_id}")
else: return self.get_show(tmdb_id) else: return self.get_show(tmdb_id)
def get_movie(self, tmdb_id, partial=None): def get_movie(self, tmdb_id):
try: return self.TMDb.movie(tmdb_id, partial=partial) try: return TMDbMovie(self, tmdb_id)
except TMDbException as e: raise Failed(f"TMDb Error: No Movie found for TMDb ID {tmdb_id}: {e}") except TMDbException as e: raise Failed(f"TMDb Error: No Movie found for TMDb ID {tmdb_id}: {e}")
def get_show(self, tmdb_id, partial=None): def get_show(self, tmdb_id):
try: return self.TMDb.tv_show(tmdb_id, partial=partial) try: return TMDbShow(self, tmdb_id)
except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}") except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}")
def get_collection(self, tmdb_id, partial=None): def get_collection(self, tmdb_id, partial=None):
@ -139,7 +228,7 @@ class TMDb:
except TMDbException as e: raise Failed(f"TMDb Error: No List found for TMDb ID {tmdb_id}: {e}") except TMDbException as e: raise Failed(f"TMDb Error: No List found for TMDb ID {tmdb_id}: {e}")
def get_popular_people(self, limit): def get_popular_people(self, limit):
return {p.id: p.name for p in self.TMDb.popular_people().get_results(limit)} return {str(p.id): p.name for p in self.TMDb.popular_people().get_results(limit)}
def search_people(self, name): def search_people(self, name):
return self.TMDb.people_search(name) return self.TMDb.people_search(name)
@ -219,7 +308,7 @@ class TMDb:
tmdb_name = collection.name tmdb_name = collection.name
ids = [(t.id, "tmdb") for t in collection.movies] ids = [(t.id, "tmdb") for t in collection.movies]
elif method == "tmdb_show": elif method == "tmdb_show":
tmdb_name = self.get_show(tmdb_id).name tmdb_name = self.get_show(tmdb_id).title
ids.append((tmdb_id, "tmdb_show")) ids.append((tmdb_id, "tmdb_show"))
else: else:
person = self.get_person(tmdb_id, partial="movie_credits,tv_credits") person = self.get_person(tmdb_id, partial="movie_credits,tv_credits")

@ -241,7 +241,7 @@ class Trakt:
return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show") return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show")
def get_people(self, data): def get_people(self, data):
return {i[0][0]: i[0][1] for i in self._user_list(data) if i[1] == "tmdb_person"} return {str(i[0][0]): i[0][1] for i in self._user_list(data) if i[1] == "tmdb_person"}
def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"): def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"):
values = util.get_list(trakt_lists, split=False) values = util.get_list(trakt_lists, split=False)

@ -1,4 +1,5 @@
import requests, time import requests, time
from datetime import datetime
from lxml.etree import ParserError from lxml.etree import ParserError
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
@ -66,11 +67,11 @@ class TVDbObj:
else: else:
raise Failed(f"TVDb Error: Could not find a TVDb {self.media_type} ID at the URL {self.tvdb_url}") raise Failed(f"TVDb Error: Could not find a TVDb {self.media_type} ID at the URL {self.tvdb_url}")
def parse_page(xpath): def parse_page(xpath, is_list=False):
parse_results = response.xpath(xpath) parse_results = response.xpath(xpath)
if len(parse_results) > 0: if len(parse_results) > 0:
parse_results = [r.strip() for r in parse_results if len(r) > 0] parse_results = [r.strip() for r in parse_results if len(r) > 0]
return parse_results[0] if len(parse_results) > 0 else None return parse_results if is_list else parse_results[0] if len(parse_results) > 0 else None
def parse_title_summary(lang=None): def parse_title_summary(lang=None):
place = "//div[@class='change_translation_text' and " place = "//div[@class='change_translation_text' and "
@ -85,15 +86,17 @@ class TVDbObj:
if not self.title: if not self.title:
raise Failed(f"TVDb Error: Name not found from TVDb URL: {self.tvdb_url}") raise Failed(f"TVDb Error: Name not found from TVDb URL: {self.tvdb_url}")
self.poster_path = parse_page("//div[@class='row hidden-xs hidden-sm']/div/img/@src") self.poster_path = parse_page("(//h2[@class='mt-4' and text()='Posters']/following::div/a/@href)[1]")
self.background_path = parse_page("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]") self.background_path = parse_page("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]")
if self.is_movie: if self.is_movie:
self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]") self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]", is_list=True)
self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]") self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]", is_list=True)
self.studios = parse_page("//strong[text()='Studio']/parent::li/span/a/text()[normalize-space()]") self.studios = parse_page("//strong[text()='Studio']/parent::li/span/a/text()[normalize-space()]", is_list=True)
self.released = datetime.strptime(parse_page("//strong[text()='Released']/parent::li/span/text()[normalize-space()]"), "%B %d, %Y")
else: else:
self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]") self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]", is_list=True)
self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]") self.released = datetime.strptime(parse_page("//strong[text()='First Aired']/parent::li/span/text()[normalize-space()]"), "%B %d, %Y")
self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]", is_list=True)
tmdb_id = None tmdb_id = None
imdb_id = None imdb_id = None

@ -111,7 +111,7 @@ def get_list(data, lower=False, upper=False, split=True, int_list=False):
elif int_list is True: elif int_list is True:
try: return [int(str(d).strip()) for d in list_data] try: return [int(str(d).strip()) for d in list_data]
except ValueError: return [] except ValueError: return []
else: return [str(d).strip() for d in list_data] else: return [d if isinstance(d, dict) else str(d).strip() for d in list_data]
def get_int_list(data, id_type): def get_int_list(data, id_type):
int_values = [] int_values = []
@ -438,12 +438,14 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa
else: else:
raise Failed(f"{error} Error: {display} {dict_data} is not a dictionary") raise Failed(f"{error} Error: {display} {dict_data} is not a dictionary")
return final_list return final_list
elif datatype in ["dict", "dictlist", "dictdict"]: elif datatype in ["dict", "dictlist", "dictdict", "strdict"]:
if isinstance(value, dict): if isinstance(value, dict):
if datatype == "dict": if datatype == "dict":
return value return value
elif datatype == "dictlist": elif datatype == "dictlist":
return {k: v if isinstance(v, list) else [v] for k, v in value.items()} return {k: v if isinstance(v, list) else [v] for k, v in value.items()}
elif datatype == "strdict":
return {str(k): str(v) for k, v in value.items()}
else: else:
final_dict = {} final_dict = {}
for dict_key, dict_data in value.items(): for dict_key, dict_data in value.items():

@ -2,7 +2,7 @@ import argparse, os, sys, time, traceback
from datetime import datetime from datetime import datetime
try: try:
import plexapi, schedule import plexapi, requests, schedule
from modules.logs import MyLogger from modules.logs import MyLogger
from plexapi.exceptions import NotFound from plexapi.exceptions import NotFound
from plexapi.video import Show, Season from plexapi.video import Show, Season
@ -116,6 +116,10 @@ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) a
version = line version = line
break break
is_develop = "develop" in version
version_url = f"https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager/{'develop' if is_develop else 'master'}/VERSION"
newest_version = requests.get(version_url).content.decode().strip()
plexapi.BASE_HEADERS['X-Plex-Client-Identifier'] = "Plex-Meta-Manager" plexapi.BASE_HEADERS['X-Plex-Client-Identifier'] = "Plex-Meta-Manager"
def start(attrs): def start(attrs):
@ -129,6 +133,8 @@ def start(attrs):
logger.info_center("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ") logger.info_center("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")
logger.info_center(" |___/ ") logger.info_center(" |___/ ")
logger.info(f" Version: {version}") logger.info(f" Version: {version}")
if version != newest_version and ((is_develop and int(version[version.index("develop") + 7:]) < int(newest_version[newest_version.index("develop") + 7:])) or not is_develop):
logger.info(f" Newest Version: {newest_version}")
if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} " if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} "
elif "test" in attrs and attrs["test"]: start_type = "Test " elif "test" in attrs and attrs["test"]: start_type = "Test "
elif "collections" in attrs and attrs["collections"]: start_type = "Collections " elif "collections" in attrs and attrs["collections"]: start_type = "Collections "
@ -185,7 +191,10 @@ def start(attrs):
except Failed as e: except Failed as e:
logger.stacktrace() logger.stacktrace()
logger.error(f"Webhooks Error: {e}") logger.error(f"Webhooks Error: {e}")
logger.separator(f"Finished {start_type}Run\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}") version_line = f"Version: {version}"
if version != newest_version and ((is_develop and int(version[version.index("develop") + 7:]) < int(newest_version[newest_version.index("develop") + 7:])) or not is_develop):
version_line = f"{version_line} Newest Version: {newest_version}"
logger.separator(f"Finished {start_type}Run\n{version_line}\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}")
logger.remove_main_handler() logger.remove_main_handler()
def update_libraries(config): def update_libraries(config):
@ -407,6 +416,8 @@ def library_operations(config, library):
logger.debug(f"Mass Audience Rating Update: {library.mass_audience_rating_update}") logger.debug(f"Mass Audience Rating Update: {library.mass_audience_rating_update}")
logger.debug(f"Mass Critic Rating Update: {library.mass_critic_rating_update}") logger.debug(f"Mass Critic Rating Update: {library.mass_critic_rating_update}")
logger.debug(f"Mass Content Rating Update: {library.mass_content_rating_update}") logger.debug(f"Mass Content Rating Update: {library.mass_content_rating_update}")
logger.debug(f"Mass Originally Available Update: {library.mass_originally_available_update}")
logger.debug(f"Mass IMDb Parental Labels: {library.mass_imdb_parental_labels}")
logger.debug(f"Mass Trakt Rating Update: {library.mass_trakt_rating_update}") logger.debug(f"Mass Trakt Rating Update: {library.mass_trakt_rating_update}")
logger.debug(f"Mass Collection Mode Update: {library.mass_collection_mode}") logger.debug(f"Mass Collection Mode Update: {library.mass_collection_mode}")
logger.debug(f"Split Duplicates: {library.split_duplicates}") logger.debug(f"Split Duplicates: {library.split_duplicates}")
@ -418,6 +429,7 @@ def library_operations(config, library):
logger.debug(f"TMDb Collections: {library.tmdb_collections}") logger.debug(f"TMDb Collections: {library.tmdb_collections}")
logger.debug(f"Genre Collections: {library.genre_collections}") logger.debug(f"Genre Collections: {library.genre_collections}")
logger.debug(f"Genre Mapper: {library.genre_mapper}") logger.debug(f"Genre Mapper: {library.genre_mapper}")
logger.debug(f"Content Rating Mapper: {library.content_rating_mapper}")
logger.debug(f"Metadata Backup: {library.metadata_backup}") logger.debug(f"Metadata Backup: {library.metadata_backup}")
logger.debug(f"Item Operation: {library.items_library_operation}") logger.debug(f"Item Operation: {library.items_library_operation}")
logger.debug("") logger.debug("")
@ -446,6 +458,11 @@ def library_operations(config, library):
sonarr_adds = [] sonarr_adds = []
trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else [] trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else []
reverse_anidb = {}
if library.mass_genre_update == "anidb":
for k, v in library.anidb_map.values():
reverse_anidb[v] = k
for i, item in enumerate(items, 1): for i, item in enumerate(items, 1):
try: try:
library.reload(item) library.reload(item)
@ -471,6 +488,14 @@ def library_operations(config, library):
except Failed: except Failed:
pass pass
if library.mass_imdb_parental_labels:
try:
parental_guide = config.IMDb.parental_guide(imdb_id)
labels = [f"{k.capitalize()}:{v}" for k, v in parental_guide.items() if library.mass_imdb_parental_labels == "with_none" or v != "None"]
library.edit_tags("label", item, append_tags=labels)
except Failed:
pass
path = os.path.dirname(str(item.locations[0])) if library.is_movie else str(item.locations[0]) path = os.path.dirname(str(item.locations[0])) if library.is_movie else str(item.locations[0])
if library.Radarr and library.radarr_add_all_existing and tmdb_id: if library.Radarr and library.radarr_add_all_existing and tmdb_id:
path = path.replace(library.Radarr.plex_path, library.Radarr.radarr_path) path = path.replace(library.Radarr.plex_path, library.Radarr.radarr_path)
@ -482,12 +507,14 @@ def library_operations(config, library):
sonarr_adds.append((tvdb_id, path)) sonarr_adds.append((tvdb_id, path))
tmdb_item = None tmdb_item = None
if library.tmdb_collections or library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" or library.mass_critic_rating_update == "tmdb": if library.tmdb_collections or library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" \
or library.mass_critic_rating_update == "tmdb" or library.mass_originally_available_update == "tmdb":
tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.is_movie) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.is_movie)
omdb_item = None omdb_item = None
if library.mass_genre_update == "omdb" or library.mass_audience_rating_update == "omdb" \ if library.mass_genre_update == "omdb" or library.mass_audience_rating_update == "omdb" \
or library.mass_critic_rating_update == "omdb" or library.mass_content_rating_update == "omdb": or library.mass_critic_rating_update == "omdb" or library.mass_content_rating_update == "omdb" \
or library.mass_originally_available_update == "omdb":
if config.OMDb.limit is False: if config.OMDb.limit is False:
if tmdb_id and not imdb_id: if tmdb_id and not imdb_id:
imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) imdb_id = config.Convert.tmdb_to_imdb(tmdb_id)
@ -505,7 +532,7 @@ def library_operations(config, library):
logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}") logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}")
tvdb_item = None tvdb_item = None
if library.mass_genre_update == "tvdb": if library.mass_genre_update == "tvdb" or library.mass_originally_available_update == "tvdb":
if tvdb_id: if tvdb_id:
try: try:
tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie) tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie)
@ -514,9 +541,26 @@ def library_operations(config, library):
else: else:
logger.info(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}") logger.info(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}")
anidb_item = None
if library.mass_genre_update == "anidb":
if item.ratingKey in reverse_anidb:
anidb_id = reverse_anidb[item.ratingKey]
elif tvdb_id in config.Convert._tvdb_to_anidb:
anidb_id = config.Convert._tvdb_to_anidb[tvdb_id]
elif imdb_id in config.Convert._imdb_to_anidb:
anidb_id = config.Convert._imdb_to_anidb[imdb_id]
else:
anidb_id = None
logger.info(f"{item.title[:25]:<25} | No AniDB ID for Guid: {item.guid}")
if anidb_id:
try:
anidb_item = config.AniDB.get_anime(anidb_id)
except Failed as e:
logger.error(str(e))
mdb_item = None mdb_item = None
if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \ if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \
or library.mass_content_rating_update in ["mdb", "mdb_commonsense"]: or library.mass_content_rating_update in ["mdb", "mdb_commonsense"] or library.mass_originally_available_update == "mdb":
if config.Mdblist.limit is False: if config.Mdblist.limit is False:
if tmdb_id and not imdb_id: if tmdb_id and not imdb_id:
imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) imdb_id = config.Convert.tmdb_to_imdb(tmdb_id)
@ -533,8 +577,8 @@ def library_operations(config, library):
else: else:
logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}") logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}")
if library.tmdb_collections and tmdb_item and tmdb_item.collection: if library.tmdb_collections and tmdb_item and tmdb_item.collection_id:
tmdb_collections[tmdb_item.collection.id] = tmdb_item.collection.name tmdb_collections[tmdb_item.collection_id] = tmdb_item.collection_name
def get_rating(attribute): def get_rating(attribute):
if tmdb_item and attribute == "tmdb": if tmdb_item and attribute == "tmdb":
@ -559,19 +603,40 @@ def library_operations(config, library):
return mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None return mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None
elif mdb_item and attribute == "mdb_letterboxd": elif mdb_item and attribute == "mdb_letterboxd":
return mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None return mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None
elif anidb_item and attribute == "anidb_rating":
return anidb_item.rating
elif anidb_item and attribute == "anidb_average":
return anidb_item.average
else: else:
raise Failed raise Failed
if library.mass_genre_update: if library.mass_genre_update or library.genre_mapper:
try: try:
if tmdb_item and library.mass_genre_update == "tmdb": new_genres = []
new_genres = [genre.name for genre in tmdb_item.genres] if library.mass_genre_update:
elif omdb_item and library.mass_genre_update == "omdb": if tmdb_item and library.mass_genre_update == "tmdb":
new_genres = omdb_item.genres new_genres = tmdb_item.genres
elif tvdb_item and library.mass_genre_update == "tvdb": elif omdb_item and library.mass_genre_update == "omdb":
new_genres = tvdb_item.genres new_genres = omdb_item.genres
else: elif tvdb_item and library.mass_genre_update == "tvdb":
raise Failed new_genres = tvdb_item.genres
elif anidb_item and library.mass_genre_update == "anidb":
new_genres = anidb_item.genres
else:
raise Failed
if not new_genres:
logger.info(f"{item.title[:25]:<25} | No Genres Found")
if library.genre_mapper:
if not new_genres:
new_genres = [g.tag for g in item.genres]
mapped_genres = []
for genre in new_genres:
if genre in library.genre_mapper:
if library.genre_mapper[genre]:
mapped_genres.append(library.genre_mapper[genre])
else:
mapped_genres.append(genre)
new_genres = mapped_genres
library.edit_tags("genre", item, sync_tags=new_genres) library.edit_tags("genre", item, sync_tags=new_genres)
except Failed: except Failed:
pass pass
@ -595,35 +660,49 @@ def library_operations(config, library):
logger.info(f"{item.title[:25]:<25} | Critic Rating | {new_rating}") logger.info(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")
except Failed: except Failed:
pass pass
if library.mass_content_rating_update: if library.mass_content_rating_update or library.content_rating_mapper:
try: try:
if omdb_item and library.mass_content_rating_update == "omdb": new_rating = None
new_rating = omdb_item.content_rating if library.mass_content_rating_update:
elif mdb_item and library.mass_content_rating_update == "mdb": if omdb_item and library.mass_content_rating_update == "omdb":
new_rating = mdb_item.certification if mdb_item.certification else None new_rating = omdb_item.content_rating
elif mdb_item and library.mass_content_rating_update == "mdb_commonsense": elif mdb_item and library.mass_content_rating_update == "mdb":
new_rating = mdb_item.commonsense if mdb_item.commonsense else None new_rating = mdb_item.certification if mdb_item.certification else None
else: elif mdb_item and library.mass_content_rating_update == "mdb_commonsense":
raise Failed new_rating = mdb_item.commonsense if mdb_item.commonsense else None
if new_rating is None: else:
logger.info(f"{item.title[:25]:<25} | No Content Rating Found") raise Failed
elif str(item.rating) != str(new_rating): if new_rating is None:
logger.info(f"{item.title[:25]:<25} | No Content Rating Found")
if library.content_rating_mapper:
if new_rating is None:
new_rating = item.contentRating
if new_rating in library.content_rating_mapper:
new_rating = library.content_rating_mapper[new_rating]
if str(item.contentRating) != str(new_rating):
library.edit_query(item, {"contentRating.value": new_rating, "contentRating.locked": 1}) library.edit_query(item, {"contentRating.value": new_rating, "contentRating.locked": 1})
logger.info(f"{item.title[:25]:<25} | Content Rating | {new_rating}") logger.info(f"{item.title[:25]:<25} | Content Rating | {new_rating}")
except Failed: except Failed:
pass pass
if library.mass_originally_available_update:
if library.genre_mapper:
try: try:
adds = [] if omdb_item and library.mass_originally_available_update == "omdb":
deletes = [] new_date = omdb_item.released
library.reload(item) elif mdb_item and library.mass_originally_available_update == "mdb":
for genre in item.genres: new_date = mdb_item.released
if genre.tag in library.genre_mapper: elif tvdb_item and library.mass_originally_available_update == "tvdb":
deletes.append(genre.tag) new_date = tvdb_item.released
if library.genre_mapper[genre.tag]: elif tmdb_item and library.mass_originally_available_update == "tmdb":
adds.append(library.genre_mapper[genre.tag]) new_date = tmdb_item.release_date if library.is_movie else tmdb_item.first_air_date
library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) elif anidb_item and library.mass_originally_available_update == "anidb":
new_date = anidb_item.released
else:
raise Failed
if new_date is None:
logger.info(f"{item.title[:25]:<25} | No Originally Available Date Found")
elif str(item.rating) != str(new_date):
library.edit_query(item, {"originallyAvailableAt.value": new_date.strftime("%Y-%m-%d"), "originallyAvailableAt.locked": 1})
logger.info(f"{item.title[:25]:<25} | Originally Available Date | {new_date.strftime('%Y-%m-%d')}")
except Failed: except Failed:
pass pass
@ -737,7 +816,7 @@ def library_operations(config, library):
logger.info("") logger.info("")
logger.info(f"Metadata Backup Path: {library.metadata_backup['path']}") logger.info(f"Metadata Backup Path: {library.metadata_backup['path']}")
logger.info("") logger.info("")
meta = {} meta = None
if os.path.exists(library.metadata_backup["path"]): if os.path.exists(library.metadata_backup["path"]):
try: try:
meta, _, _ = yaml.util.load_yaml_guess_indent(open(library.metadata_backup["path"])) meta, _, _ = yaml.util.load_yaml_guess_indent(open(library.metadata_backup["path"]))
@ -749,6 +828,8 @@ def library_operations(config, library):
i += 1 i += 1
os.rename(library.metadata_backup["path"], f"{filename}{i}{file_extension}") os.rename(library.metadata_backup["path"], f"{filename}{i}{file_extension}")
logger.error(f"Backup failed to load saving copy to {filename}{i}{file_extension}") logger.error(f"Backup failed to load saving copy to {filename}{i}{file_extension}")
if not meta:
meta = {}
if "metadata" not in meta: if "metadata" not in meta:
meta["metadata"] = {} meta["metadata"] = {}
items = library.get_all(load=True) items = library.get_all(load=True)
@ -824,7 +905,6 @@ def run_collection(config, library, metadata, requested_collections):
items_added = 0 items_added = 0
items_removed = 0 items_removed = 0
valid = True
if not builder.smart_url and builder.builders and not builder.blank_collection: if not builder.smart_url and builder.builders and not builder.blank_collection:
logger.info("") logger.info("")
logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}")
@ -853,14 +933,6 @@ def run_collection(config, library, metadata, requested_collections):
items_removed = builder.sync_collection() items_removed = builder.sync_collection()
library.stats["removed"] += items_removed library.stats["removed"] += items_removed
library.status[mapping_name]["removed"] = items_removed library.status[mapping_name]["removed"] = items_removed
elif len(builder.added_items) + builder.beginning_count < builder.minimum and builder.build_collection:
logger.info("")
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
valid = False
if builder.details["delete_below_minimum"] and builder.obj:
logger.info("")
logger.info(builder.delete())
builder.deleted = True
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
radarr_add, sonarr_add = builder.run_missing() radarr_add, sonarr_add = builder.run_missing()
@ -869,6 +941,19 @@ def run_collection(config, library, metadata, requested_collections):
library.stats["sonarr"] += sonarr_add library.stats["sonarr"] += sonarr_add
library.status[mapping_name]["sonarr"] += sonarr_add library.status[mapping_name]["sonarr"] += sonarr_add
valid = True
if builder.build_collection and (
(builder.smart_url and len(library.get_filter_items(builder.smart_url)) < builder.minimum)
or (not builder.smart_url and len(builder.added_items) + builder.beginning_count < builder.minimum)
):
logger.info("")
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
valid = False
if builder.details["delete_below_minimum"] and builder.obj:
logger.info("")
logger.info(builder.delete())
builder.deleted = True
run_item_details = True run_item_details = True
if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection): if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection):
try: try:

Loading…
Cancel
Save