diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 88d668d8..681cff27 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,11 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Check Out Repo
- uses: actions/checkout@v3
-
- name: Send Discord Release Notification
- uses: bythope/discord-webhook-messages@v1.1.0
+ uses: nhevia/discord-styled-releases@main
with:
- webhookUrl: ${{ secrets.RELEASE_WEBHOOK }}
- handler: 'release'
\ No newline at end of file
+ webhook_id: ${{ secrets.RELEASE_WEBHOOK_ID }}
+ webhook_token: ${{ secrets.RELEASE_WEBHOOK_TOKEN }}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 6362a8f4..34205f8b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,18 +1,20 @@
FROM python:3.9-slim
ENV TINI_VERSION v0.19.0
-ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
-RUN chmod +x /tini
+COPY . /
RUN echo "**** install system packages ****" \
&& apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y tzdata --no-install-recommends \
- && apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev
-COPY requirements.txt /
-RUN echo "**** install python packages ****" \
+ && apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev wget \
+ && wget -O /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-"$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
+ && chmod +x /tini \
&& 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 update \
+ && apt-get check \
+ && apt-get -f install \
+ && apt-get autoclean \
&& rm -rf /requirements.txt /tmp/* /var/tmp/* /var/lib/apt/lists/*
-COPY . /
VOLUME /config
ENTRYPOINT ["/tini", "-s", "python3", "plex_meta_manager.py", "--"]
diff --git a/VERSION b/VERSION
index 41c11ffb..4a02d2c3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.16.1
+1.16.2
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index 4b4bf150..db3d0878 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -168,4 +168,8 @@ p {
}
.tab-set {
width: auto !important;
+}
+.highlighted {
+ background-color: #ac0ce3 !important;
+ color: #FFFFFF !important;
}
\ No newline at end of file
diff --git a/docs/config/operations.md b/docs/config/operations.md
index e2674e2c..f098e202 100644
--- a/docs/config/operations.md
+++ b/docs/config/operations.md
@@ -16,32 +16,35 @@ libraries:
The available attributes for the operations attribute are as follows
-| Attribute | Description |
-|:--------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `assets_for_all` | Search in assets for images for every item in your library.
**Values:** `true` or `false` |
-| `delete_collections_with_less` | Deletes every collection with less than the given number of items.
**Values:** number greater then 0 |
-| `delete_unmanaged_collections` | Deletes every unmanaged collection
**Values:** `true` or `false` |
-| `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres
**Values:**
`tmdb` | Use TMDb for Genres |
`tvdb` | Use TVDb for Genres |
`omdb` | Use IMDb through OMDb for Genres |
|
-| `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres
**Values:** `mdb` | Use MdbList for Content Ratings |
`mdb_commonsense` | Use Commonsense Rating through MDbList for Content Ratings |
`omdb` | Use IMDb through OMDb for Content Ratings |
|
-| `mass_audience_rating_update`/
`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating
**Values:** `tmdb` | Use TMDb Rating |
`omdb` | Use IMDbRating through OMDb |
`mdb` | Use MdbList Score |
`mdb_imdb` | Use IMDb Rating through MDbList |
`mdb_metacritic` | Use Metacritic Rating through MDbList |
`mdb_metacriticuser` | Use Metacritic User Rating through MDbList |
`mdb_trakt` | Use Trakt Rating through MDbList |
`mdb_tomatoes` | Use Rotten Tomatoes Rating through MDbList |
`mdb_tomatoesaudience` | Use Rotten Tomatoes Audience Rating through MDbList |
`mdb_tmdb` | Use TMDb Rating through MDbList |
`mdb_letterboxd` | Use Letterboxd Rating through MDbList |
|
-| `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
**Values:** `true` or `false` |
-| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode
**Values:** `default`: Library default
`hide`: Hide Collection
`hide_items`: Hide Items in this Collection
`show_items`: Show this Collection and its Items`default` | Library default |
`hide` | Hide Collection |
`hide_items` | Hide Items in this Collection |
`show_items` | Show this Collection and its Items |
|
-| `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title
**Values:** `true` or `false` |
-| `split_duplicates` | Splits all duplicate movies/shows found in this library
**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.
**Values:** `true` or `false` |
-| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given
**Values:** List or comma separated string of tags |
-| `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.
**Values:** `true` or `false` |
-| `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given
**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.
**Values:** [see below for usage](#genre-mapper) |
-| `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.
**Values:** [see below for usage](#metadata-backup) |
+| Attribute | Description |
+|:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `assets_for_all` | Search in assets for images for every item in your library.
**Values:** `true` or `false` |
+| `delete_collections_with_less` | Deletes every collection with less than the given number of items.
**Values:** number greater then 0 |
+| `delete_unmanaged_collections` | Deletes every unmanaged collection
**Values:** `true` or `false` |
+| `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres
**Values:** `tmdb` | Use TMDb for Genres |
`tvdb` | Use TVDb for Genres |
`omdb` | Use IMDb through OMDb for Genres |
`anidb` | Use AniDB Tags for Genres |
|
+| `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres
**Values:** `mdb` | Use MdbList for Content Ratings |
`mdb_commonsense` | Use Commonsense Rating through MDbList for Content Ratings |
`omdb` | Use IMDb through OMDb for Content Ratings |
|
+| `mass_originally_available_update` | Updates every item's originally available date in the library to the chosen site's date
**Values:** `tmdb` | Use TMDb Release Date |
`tvdb` | Use TVDb Release Date |
`omdb` | Use IMDb Release Date through OMDb |
`mdb` | Use MdbList Release Date |
`anidb` | Use AniDB Release Date |
|
+| `mass_audience_rating_update`/
`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating
**Values:** `tmdb` | Use TMDb Rating |
`omdb` | Use IMDbRating through OMDb |
`mdb` | Use MdbList Score |
`mdb_imdb` | Use IMDb Rating through MDbList |
`mdb_metacritic` | Use Metacritic Rating through MDbList |
`mdb_metacriticuser` | Use Metacritic User Rating through MDbList |
`mdb_trakt` | Use Trakt Rating through MDbList |
`mdb_tomatoes` | Use Rotten Tomatoes Rating through MDbList |
`mdb_tomatoesaudience` | Use Rotten Tomatoes Audience Rating through MDbList |
`mdb_tmdb` | Use TMDb Rating through MDbList |
`mdb_letterboxd` | Use Letterboxd Rating through MDbList |
`anidb_rating` | Use AniDB Rating |
`anidb_average` | Use AniDB Average |
|
+| `mass_imdb_parental_labels` | Updates every item's labels in the library to match the IMDb Parental Guide
**Values** `with_none` or `without_none` |
+| `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
**Values:** `true` or `false` |
+| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode
**Values:** `default`: Library default
`hide`: Hide Collection
`hide_items`: Hide Items in this Collection
`show_items`: Show this Collection and its Items`default` | Library default |
`hide` | Hide Collection |
`hide_items` | Hide Items in this Collection |
`show_items` | Show this Collection and its Items |
|
+| `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title
**Values:** `true` or `false` |
+| `split_duplicates` | Splits all duplicate movies/shows found in this library
**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.
**Values:** `true` or `false` |
+| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given
**Values:** List or comma separated string of tags |
+| `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.
**Values:** `true` or `false` |
+| `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given
**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.
**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.
**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.
**Values:** [see below for usage](#metadata-backup) |
## Genre Mapper
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.
-* The key (`Action` in the example below) is what the genres will end up as.
-* The value(`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the key.
+* The key (`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the value.
+* 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`.
@@ -50,36 +53,58 @@ library:
Movies:
operations:
genre_mapper:
- Action: Action/Adventure, Action & Adventure
- Comedy: Romantic Comedy
+ "Action/Adventure": Action
+ "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
library:
Movies:
operations:
genre_mapper:
- Action:
- - Action/Adventure
- - Action & Adventure
- Comedy: Romantic Comedy
+ "Action/Adventure": Action
+ "Action & Adventure": Action
+ 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
library:
Movies:
operations:
- genre_mapper:
- Action: Action/Adventure, Action & Adventure
- Romantic Comedy:
+ content_rating_mapper:
+ PG: Y-10
+ "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
diff --git a/docs/config/tmdb.md b/docs/config/tmdb.md
index 4864a79f..4acc06d2 100644
--- a/docs/config/tmdb.md
+++ b/docs/config/tmdb.md
@@ -11,9 +11,10 @@ tmdb:
language: en
```
-| Attribute | Allowed Values | Default | Required |
-|:-----------|:---------------------|:-------:|:--------:|
-| `apikey` | User TMDb V3 API Key | N/A | ✅ |
-| `language` | User Language | en | ❌ |
+| Attribute | Allowed Values | Default | Required |
+|:-------------------|:--------------------------------------------------------------------------|:-------:|:--------:|
+| `apikey` | User TMDb V3 API Key | N/A | ✅ |
+| `language` | User Language | en | ❌ |
+| `cache_expiration` | Number of days before each cache mapping expires and has to be re-cached. | 60 | ❌ |
If you do not have a TMDb V3 API key please refer to this [guide](https://developers.themoviedb.org/3/getting-started/introduction).
diff --git a/docs/metadata/builders/anilist.md b/docs/metadata/builders/anilist.md
index f316e2d6..e1c78517 100644
--- a/docs/metadata/builders/anilist.md
+++ b/docs/metadata/builders/anilist.md
@@ -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 | ✅ | ✅ | ❌ |
| [`anilist_studio`](#anilist-studio) | Finds all anime specified by the AniList Studio ID | ✅ | ✅ | ❌ |
| [`anilist_id`](#anilist-id) | Finds the anime specified by the AniList ID | ✅ | ✅ | ❌ |
+| [`anilist_userlist`](#anilist-userlist) | Finds the anime in AniList User's Anime list the options are detailed below | ✅ | ✅ | ✅ |
| [`anilist_search`](#anilist-search) | Finds the anime specified by the AniList search parameters provided | ✅ | ✅ | ✅ |
## AniList Top Rated Anime
@@ -98,6 +99,31 @@ collections:
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
**Default:** `score`
**Values:**`score` | Sort by User Score |
`popularity` | Sort by Popularity |
`status` | Sort by Status |
`progress` | Sort by Progress |
`last_updated` | Sort by Last Updated |
`last_added` | Sort by Last Added |
`start_date` | Sort by Start Date |
`completed_date` | Sort by Completed Date |
|
+
+```yaml
+collections:
+ Currently Watching Anime:
+ anilist_userlist:
+ username: Username
+ list_name: Watching
+ sort_by: score
+ collection_order: custom
+ sync_mode: sync
+```
+
## AniList Search
Finds the anime specified by the AniList Search the options are detailed below.
@@ -228,4 +254,4 @@ collections:
sort_by: popular
collection_order: custom
sync_mode: sync
-```
+```
\ No newline at end of file
diff --git a/docs/metadata/dynamic.md b/docs/metadata/dynamic.md
index 23118240..a9039693 100644
--- a/docs/metadata/dynamic.md
+++ b/docs/metadata/dynamic.md
@@ -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.
+Each template is passed a template variable whose name matches the dynamic collection `type`. i.e. in the example below `<>` 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
```yaml
diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md
index a462af74..8bf25940 100644
--- a/docs/metadata/filters.md
+++ b/docs/metadata/filters.md
@@ -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 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
-| `original_language`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
+| `original_language`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `origin_country`1 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `tmdb_status`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `tmdb_type`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
diff --git a/modules/anidb.py b/modules/anidb.py
index 0c0a4539..7b76ffaa 100644
--- a/modules/anidb.py
+++ b/modules/anidb.py
@@ -1,4 +1,5 @@
import time
+from datetime import datetime
from modules import util
from modules.util import Failed
@@ -14,9 +15,51 @@ urls = {
"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:
- def __init__(self, config):
+ def __init__(self, config, language):
self.config = config
+ self.language = language
self.username = 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"):
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:
logger.debug(f"URL: {url}")
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:
- 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):
- response = self._request(urls["popular"], language=language)
+ def _popular(self):
+ response = self._request(urls["popular"])
return util.get_int_list(response.xpath("//td[@class='name anime']/a/@href"), "AniDB ID")
- def _relations(self, anidb_id, language):
- response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}", language=language)
+ def _relations(self, anidb_id):
+ response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}")
return util.get_int_list(response.xpath("//area/@href"), "AniDB ID")
- def _validate(self, anidb_id, language):
- response = self._request(f"{urls['anime']}/{anidb_id}", language=language)
+ def _validate(self, anidb_id):
+ response = self._request(f"{urls['anime']}/{anidb_id}")
ids = response.xpath(f"//*[text()='a{anidb_id}']/text()")
if len(ids) > 0:
return util.regex_first_int(ids[0], "AniDB ID")
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_values = []
for anidb_id in anidb_list:
try:
- anidb_values.append(self._validate(anidb_id, language))
+ anidb_values.append(self._validate(anidb_id))
except Failed as e:
logger.error(e)
if len(anidb_values) > 0:
return anidb_values
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 = []
current_url = f"{urls['tag']}/{tag}"
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"))
next_page_list = response.xpath("//li[@class='next']/a/@href")
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]}"
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 = []
if method == "anidb_popular":
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":
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":
logger.info(f"Processing AniDB ID: {data}")
anidb_ids.append(data)
elif method == "anidb_relation":
logger.info(f"Processing AniDB Relation: {data}")
- anidb_ids.extend(self._relations(data, language))
+ anidb_ids.extend(self._relations(data))
else:
raise Failed(f"AniDB Error: Method {method} not supported")
logger.debug("")
diff --git a/modules/anilist.py b/modules/anilist.py
index ac571ced..757f2475 100644
--- a/modules/anilist.py
+++ b/modules/anilist.py
@@ -4,8 +4,12 @@ from modules.util import Failed
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_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 = {
"year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory",
"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"]
searches = mod_searches + no_mod_searches
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_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"}
@@ -210,6 +219,44 @@ class AniList:
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):
valid = []
for d in util.get_list(data):
@@ -243,6 +290,9 @@ class AniList:
elif method == "anilist_relations":
anilist_ids, _, name = self._relations(data)
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:
if method == "anilist_popular":
data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"}
diff --git a/modules/builder.py b/modules/builder.py
index 50487fcf..32e6f40e 100644
--- a/modules/builder.py
+++ b/modules/builder.py
@@ -169,7 +169,7 @@ all_filters = boolean_filters + special_filters + \
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
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",
"tvdb_list", "imdb_chart", "imdb_list", "stevenlu_popular", "anidb_popular",
"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_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all",
"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_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")
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")
- 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")
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()}")
@@ -934,7 +934,7 @@ class CollectionBuilder:
if method_name == "anidb_popular":
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"]:
- 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))
elif method_name == "anidb_tag":
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))
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)))
+ 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":
if self.current_time.month in [12, 1, 2]: current_season = "winter"
elif self.current_time.month in [3, 4, 5]: current_season = "spring"
@@ -1318,7 +1326,7 @@ class CollectionBuilder:
elif "tautulli" in method:
ids = self.library.Tautulli.get_rating_keys(self.library, value, self.playlist)
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)
elif "anilist" in method:
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"
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"
- 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"
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"
@@ -1898,9 +1906,9 @@ class CollectionBuilder:
try:
if item is None:
if is_movie:
- item = self.config.TMDb.get_movie(item_id, partial="keywords")
+ item = self.config.TMDb.get_movie(item_id)
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:
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:
@@ -1913,7 +1921,7 @@ class CollectionBuilder:
elif filter_attr == "tmdb_type":
check_value = discover_types[item.type]
elif filter_attr == "original_language":
- check_value = item.original_language.iso_639_1
+ check_value = item.language_iso
else:
raise Failed
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
elif filter_attr in ["tmdb_genre", "tmdb_keyword", "origin_country"]:
if filter_attr == "tmdb_genre":
- attrs = [g.name for g in item.genres]
+ attrs = item.genres
elif filter_attr == "tmdb_keyword":
- attrs = [k.name for k in item.keywords]
+ attrs = item.keywords
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:
raise Failed
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"]:
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"]
- 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"]
if visible_library is not None or visible_home is not None or visible_shared is not None:
diff --git a/modules/cache.py b/modules/cache.py
index f16d9387..2b9f173b 100644
--- a/modules/cache.py
+++ b/modules/cache.py
@@ -22,7 +22,9 @@ class Cache:
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 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_data2")
cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids_map (
key INTEGER PRIMARY KEY,
@@ -70,11 +72,12 @@ class Cache:
expiration_date TEXT)"""
)
cursor.execute(
- """CREATE TABLE IF NOT EXISTS omdb_data2 (
+ """CREATE TABLE IF NOT EXISTS omdb_data3 (
key INTEGER PRIMARY KEY,
imdb_id TEXT UNIQUE,
title TEXT,
year INTEGER,
+ released TEXT,
content_rating TEXT,
genres TEXT,
imdb_rating REAL,
@@ -87,11 +90,12 @@ class Cache:
expiration_date TEXT)"""
)
cursor.execute(
- """CREATE TABLE IF NOT EXISTS mdb_data (
+ """CREATE TABLE IF NOT EXISTS mdb_data2 (
key INTEGER PRIMARY KEY,
key_id TEXT UNIQUE,
title TEXT,
year INTEGER,
+ released TEXT,
type TEXT,
imdbid TEXT,
traktid INTEGER,
@@ -109,6 +113,56 @@ class Cache:
certification 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(
"""CREATE TABLE IF NOT EXISTS anime_map (
key INTEGER PRIMARY KEY,
@@ -149,6 +203,17 @@ class Cache:
media_id 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'")
if cursor.fetchone()[0] > 0:
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} = ?"
cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), val1))
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))
def query_omdb(self, imdb_id, expiration):
@@ -276,12 +341,13 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
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()
if row:
omdb_dict["imdbID"] = row["imdb_id"] if row["imdb_id"] else None
omdb_dict["Title"] = row["title"] if row["title"] 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["Genre"] = row["genres"] if row["genres"] 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:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
- cursor.execute("INSERT OR IGNORE INTO omdb_data2(imdb_id) VALUES(?)", (omdb.imdb_id,))
- update_sql = "UPDATE omdb_data2 SET title = ?, year = ?, content_rating = ?, genres = ?, " \
+ cursor.execute("INSERT OR IGNORE INTO omdb_data3(imdb_id) VALUES(?)", (omdb.imdb_id,))
+ update_sql = "UPDATE omdb_data3 SET title = ?, year = ?, released = ?, content_rating = ?, genres = ?, " \
"imdb_rating = ?, imdb_votes = ?, metacritic_rating = ?, type = ?, series_id = ?, " \
"season_num = ?, episode_num = ?, expiration_date = ? WHERE imdb_id = ?"
- cursor.execute(update_sql, (omdb.title, omdb.year, omdb.content_rating, omdb.genres_str,
- omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type,
- omdb.series_id, omdb.season_num, omdb.episode_num,
- expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id))
+ cursor.execute(update_sql, (
+ omdb.title, omdb.year, omdb.released.strftime("%d %b %Y"), omdb.content_rating, omdb.genres_str,
+ omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, omdb.series_id, omdb.season_num,
+ omdb.episode_num, expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id))
def query_mdb(self, key_id, expiration):
mdb_dict = {}
@@ -317,11 +383,12 @@ class Cache:
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
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()
if row:
mdb_dict["title"] = row["title"] if row["title"] 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["imdbid"] = row["imdbid"] if row["imdbid"] 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:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
- cursor.execute("INSERT OR IGNORE INTO mdb_data(key_id) VALUES(?)", (key_id,))
- update_sql = "UPDATE mdb_data SET title = ?, year = ?, type = ?, imdbid = ?, traktid = ?, " \
+ cursor.execute("INSERT OR IGNORE INTO mdb_data2(key_id) VALUES(?)", (key_id,))
+ update_sql = "UPDATE mdb_data2 SET title = ?, year = ?, released = ?, type = ?, imdbid = ?, traktid = ?, " \
"tmdbid = ?, score = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \
"trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \
"letterboxd_rating = ?, certification = ?, commonsense = ?, expiration_date = ? WHERE key_id = ?"
cursor.execute(update_sql, (
- mdb.title, mdb.year, mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid, mdb.score, mdb.imdb_rating,
- mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating, mdb.tomatoes_rating,
- mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating, mdb.content_rating,
- mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id
+ mdb.title, mdb.year, mdb.released.strftime("%Y-%m-%d"), mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid,
+ mdb.score, mdb.imdb_rating, mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating,
+ mdb.tomatoes_rating, mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating,
+ 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):
@@ -537,3 +706,33 @@ class Cache:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
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))
\ No newline at end of file
diff --git a/modules/config.py b/modules/config.py
index 5cb4e5f4..b6fe822d 100644
--- a/modules/config.py
+++ b/modules/config.py
@@ -31,8 +31,10 @@ from ruamel import yaml
logger = util.logger
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_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 = {
"tmdb": "Use TMDb Rating",
"omdb": "Use IMDb Rating through OMDb",
@@ -44,7 +46,9 @@ mass_rating_options = {
"mdb_tomatoes": "Use Rotten Tomatoes Rating through MDbList",
"mdb_tomatoesaudience": "Use Rotten Tomatoes Audience 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:
@@ -366,7 +370,8 @@ class ConfigFile:
logger.info("Connecting to TMDb...")
self.TMDb = TMDb(self, {
"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'}")
else:
@@ -380,7 +385,7 @@ class ConfigFile:
try:
self.OMDb = OMDb(self, {
"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:
self.errors.append(e)
@@ -397,7 +402,7 @@ class ConfigFile:
try:
self.Mdblist.add_key(
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")
except Failed as e:
@@ -445,7 +450,7 @@ class ConfigFile:
else:
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:
logger.separator()
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),
"tmdb_collections": None,
"genre_mapper": None,
+ "content_rating_mapper": None,
"radarr_remove_by_tag": None,
"sonarr_remove_by_tag": None,
"mass_collection_mode": None,
"metadata_backup": None,
"genre_collections": 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"]
@@ -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["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["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_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)
@@ -660,13 +668,17 @@ class ConfigFile:
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)
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"]:
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"]:
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"]:
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"]:
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"]:
@@ -722,18 +734,24 @@ class ConfigFile:
logger.error("Config Error: tmdb_collections blank using default settings")
if "genre_mapper" in lib["operations"]:
if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict):
- params["genre_mapper"] = {}
- for new_genre, old_genres in lib["operations"]["genre_mapper"].items():
- if old_genres is None:
- params["genre_mapper"][new_genre] = old_genres
+ params["genre_mapper"] = lib["operations"]["genre_mapper"]
+ for old_genre, new_genre in lib["operations"]["genre_mapper"].items():
+ if old_genre == new_genre:
+ logger.error("Config Error: genres cannot be mapped to themselves")
else:
- for old_genre in util.get_list(old_genres):
- if old_genre == new_genre:
- logger.error("Config Error: genres cannot be mapped to themselves")
- else:
- params["genre_mapper"][old_genre] = new_genre
+ params["genre_mapper"][old_genre] = new_genre if new_genre else None
else:
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"]:
params["genre_collections"] = {
"exclude_genres": [],
@@ -769,20 +787,12 @@ class ConfigFile:
self.errors.append(err)
logger.error(err)
- if self.OMDb is None and params["mass_genre_update"] == "omdb":
- error_check("mass_genre_update", "OMDb")
- if self.OMDb is None and params["mass_audience_rating_update"] == "omdb":
- error_check("mass_audience_rating_update", "OMDb")
- if self.OMDb is None and params["mass_critic_rating_update"] == "omdb":
- 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")
+ for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]:
+ if params[mass_key] == "omdb" and self.OMDb is None:
+ error_check(mass_key, "OMDb")
+ if params[mass_key] and params[mass_key].startswith("mdb") and not self.Mdblist.has_key:
+ error_check(mass_key, "MdbList API")
+
if self.Trakt is None and params["mass_trakt_rating_update"]:
error_check("mass_trakt_rating_update", "Trakt")
diff --git a/modules/convert.py b/modules/convert.py
index 7dc1ecb5..e1b01046 100644
--- a/modules/convert.py
+++ b/modules/convert.py
@@ -10,52 +10,39 @@ anime_lists_url = "https://raw.githubusercontent.com/Fribb/anime-lists/master/an
class Convert:
def __init__(self, config):
self.config = config
- self._loaded = False
self._anidb_ids = {}
self._mal_to_anidb = {}
self._anilist_to_anidb = {}
self._anidb_to_imdb = {}
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 anidb_ids(self):
- self._load_anime_conversion()
- return self._anidb_ids
-
- @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 imdb_to_anidb(self, imdb_id):
+ if imdb_id in self._imdb_to_anidb:
+ return self._imdb_to_anidb[imdb_id]
+ else:
+ raise Failed(f"AniDB ID not found for IMDb ID: {imdb_id}")
- def _load_anime_conversion(self):
- if not self._loaded:
- 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"])
- if "thetvdb_id" in anime_id:
- self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"])
- self._loaded = True
+ def tvdb_to_anidb(self, tvdb_id):
+ if int(tvdb_id) in self._tvdb_to_anidb:
+ return self._tvdb_to_anidb[int(tvdb_id)]
+ else:
+ raise Failed(f"AniDB ID not found for TVDb ID: {tvdb_id}")
def anidb_to_ids(self, anidb_ids, library):
ids = []
@@ -63,18 +50,18 @@ class Convert:
for anidb_id in anidb_list:
if anidb_id in library.anidb_map:
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
- 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)
if tmdb and tmdb_type == "movie":
ids.append((tmdb, "tmdb"))
added = True
- if added is False and anidb_id in self.anidb_to_tvdb:
- ids.append((self.anidb_to_tvdb[anidb_id], "tvdb"))
- elif anidb_id in self.anidb_to_tvdb:
- ids.append((self.anidb_to_tvdb[anidb_id], "tvdb"))
- elif anidb_id in self.anidb_ids:
+ if added is False and anidb_id in self._anidb_to_tvdb:
+ ids.append((self._anidb_to_tvdb[anidb_id], "tvdb"))
+ elif anidb_id in self._anidb_to_tvdb:
+ ids.append((self._anidb_to_tvdb[anidb_id], "tvdb"))
+ elif anidb_id in self._anidb_ids:
logger.warning(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}")
else:
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):
anidb_ids = []
for anilist_id in anilist_ids:
- if anilist_id in self.anilist_to_anidb:
- anidb_ids.append(self.anilist_to_anidb[anilist_id])
+ if anilist_id in self._anilist_to_anidb:
+ anidb_ids.append(self._anilist_to_anidb[anilist_id])
else:
logger.warning(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}")
return self.anidb_to_ids(anidb_ids, library)
@@ -94,8 +81,8 @@ class Convert:
for mal_id in mal_ids:
if int(mal_id) in library.mal_map:
ids.append((library.mal_map[int(mal_id)], "ratingKey"))
- elif int(mal_id) in self.mal_to_anidb:
- ids.extend(self.anidb_to_ids(self.mal_to_anidb[int(mal_id)], library))
+ elif int(mal_id) in self._mal_to_anidb:
+ ids.extend(self.anidb_to_ids(self._mal_to_anidb[int(mal_id)], library))
else:
logger.warning(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}")
return ids
@@ -275,26 +262,26 @@ class Convert:
raise Failed(f"Hama Agent ID: {check_id} not supported")
elif item_type == "myanimelist":
library.mal_map[int(check_id)] = item.ratingKey
- if int(check_id) in self.mal_to_anidb:
- anidb_id = self.mal_to_anidb[int(check_id)]
+ if int(check_id) in self._mal_to_anidb:
+ anidb_id = self._mal_to_anidb[int(check_id)]
else:
raise Failed(f"AniDB ID not found for MyAnimeList ID: {check_id}")
elif item_type == "local": raise Failed("No match in Plex")
else: raise Failed(f"Agent {item_type} not supported")
if anidb_id:
- if anidb_id in self.anidb_to_imdb:
+ if anidb_id in self._anidb_to_imdb:
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)
if tmdb and tmdb_type == "movie":
imdb_id.append(imdb)
tmdb_id.append(tmdb)
added = True
- if added is False and anidb_id in self.anidb_to_tvdb:
- tvdb_id.append(self.anidb_to_tvdb[anidb_id])
- elif anidb_id in self.anidb_to_tvdb:
- tvdb_id.append(self.anidb_to_tvdb[anidb_id])
+ if added is False and anidb_id in self._anidb_to_tvdb:
+ tvdb_id.append(self._anidb_to_tvdb[anidb_id])
+ elif anidb_id in self._anidb_to_tvdb:
+ tvdb_id.append(self._anidb_to_tvdb[anidb_id])
else:
raise Failed(f"AniDB: {anidb_id} not found")
else:
diff --git a/modules/imdb.py b/modules/imdb.py
index 39e8dbd3..2fb73750 100644
--- a/modules/imdb.py
+++ b/modules/imdb.py
@@ -128,6 +128,24 @@ class IMDb:
return imdb_ids
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):
if chart == "box_office":
url = "chart/boxoffice"
diff --git a/modules/library.py b/modules/library.py
index 9ad5e2d0..a4be913d 100644
--- a/modules/library.py
+++ b/modules/library.py
@@ -72,6 +72,8 @@ class Library(ABC):
self.mass_audience_rating_update = params["mass_audience_rating_update"]
self.mass_critic_rating_update = params["mass_critic_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.radarr_add_all_existing = params["radarr_add_all_existing"]
self.radarr_remove_by_tag = params["radarr_remove_by_tag"]
@@ -83,6 +85,7 @@ class Library(ABC):
self.tmdb_collections = params["tmdb_collections"]
self.genre_collections = params["genre_collections"]
self.genre_mapper = params["genre_mapper"]
+ self.content_rating_mapper = params["content_rating_mapper"]
self.error_webhooks = params["error_webhooks"]
self.changes_webhooks = params["changes_webhooks"]
self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex?
@@ -93,8 +96,8 @@ class Library(ABC):
self.status = {}
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.genre_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False
+ 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.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 \
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
diff --git a/modules/mdblist.py b/modules/mdblist.py
index a1333036..23a13460 100644
--- a/modules/mdblist.py
+++ b/modules/mdblist.py
@@ -1,3 +1,4 @@
+from datetime import datetime
from modules import util
from modules.util import Failed
from urllib.parse import urlparse
@@ -17,6 +18,10 @@ class MDbObj:
self._data = data
self.title = data["title"]
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.imdbid = data["imdbid"]
self.traktid = util.check_num(data["traktid"])
diff --git a/modules/meta.py b/modules/meta.py
index 045c2a2d..5060a33e 100644
--- a/modules/meta.py
+++ b/modules/meta.py
@@ -246,9 +246,11 @@ class MetadataFile(DataFile):
all_items = None
if self.dynamic_collections:
logger.info("")
- logger.separator(f"Dynamic Collections")
- logger.info("")
+ logger.separator("Dynamic Collections")
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:
methods = {dm.lower(): dm for dm in dynamic}
if "type" not in methods:
@@ -268,11 +270,11 @@ class MetadataFile(DataFile):
if og_exclude and include:
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 {}
- exclude = [e for e in og_exclude]
+ exclude = [str(e) for e in og_exclude]
for k, v in addons.items():
if k in v:
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 = "<>"
default_template = None
auto_list = {}
@@ -283,10 +285,10 @@ class MetadataFile(DataFile):
auto_list[ck] = cv
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
- if auto_type in ["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}
+ if auto_type in ["decade", "subtitle_language", "audio_language"]:
+ 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:
- 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:
default_template = {"smart_filter": {"limit": 50, "sort_by": "plays.desc", "any": {f"artist_{auto_type}": f"<<{auto_type}>>"}}}
default_title_format = "Most Played <> <>s"
@@ -303,8 +305,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
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)
- if tmdb_item and tmdb_item.collection 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
+ 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[str(tmdb_item.collection_id)] = tmdb_item.collection_name
logger.exorcise()
elif auto_type == "original_language":
if not all_items:
@@ -313,8 +315,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
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")
- 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:
- auto_list[tmdb_item.original_language.iso_639_1] = tmdb_item.original_language.english_name
+ 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.language_iso] = tmdb_item.language_name
logger.exorcise()
default_title_format = "<> <>s"
elif auto_type == "origin_country":
@@ -324,8 +326,8 @@ class MetadataFile(DataFile):
logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}")
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")
- if tmdb_item and tmdb_item.origin_countries:
- for country in tmdb_item.origin_countries:
+ if tmdb_item and tmdb_item.countries:
+ for country in tmdb_item.countries:
if country.iso_3166_1 not in exclude and country.name not in exclude:
auto_list[country.iso_3166_1] = country.name
logger.exorcise()
@@ -369,7 +371,7 @@ class MetadataFile(DataFile):
try:
results = self.config.TMDb.search_people(role["name"])
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
except TMDbNotFound:
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")
if "pre_format_override" in methods:
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 {}
- key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="dict") if "key_name_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="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
sync = util.parse("Config", "sync", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "sync" in methods else False
if "<>" in title_format:
@@ -445,23 +447,28 @@ class MetadataFile(DataFile):
if key not in exclude:
other_keys.append(key)
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():
if key in v:
template_call[k] = v[key]
if key in title_override:
collection_title = title_override[key]
else:
- if key in key_name_override:
- 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("<>", value).replace("<>", value)
+ collection_title = title_format.replace("<>", key_name).replace("<>", key_name)
if collection_title in col_names:
logger.warning(f"Config Warning: Skipping duplicate collection: {collection_title}")
else:
@@ -662,18 +669,14 @@ class MetadataFile(DataFile):
genres = []
if tmdb_item:
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
- elif not tmdb_is_movie and tmdb_item.original_name != tmdb_item.name:
- original_title = tmdb_item.original_name
rating = tmdb_item.vote_average
- if tmdb_is_movie and tmdb_item.companies:
- studio = tmdb_item.companies[0].name
- elif not tmdb_is_movie and tmdb_item.networks:
- studio = tmdb_item.networks[0].name
+ studio = tmdb_item.studio
tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None
summary = tmdb_item.overview
- genres = [genre.name for genre in tmdb_item.genres]
+ genres = tmdb_item.genres
edits = {}
add_edit("title", item, meta, methods, value=title)
diff --git a/modules/omdb.py b/modules/omdb.py
index 1cf954cc..7d7a0ca7 100644
--- a/modules/omdb.py
+++ b/modules/omdb.py
@@ -1,3 +1,4 @@
+from datetime import datetime
from modules import util
from modules.util import Failed
@@ -11,40 +12,33 @@ class OMDbObj:
self._data = data
if data["Response"] == "False":
raise Failed(f"OMDb Error: {data['Error']} IMDb ID: {imdb_id}")
- self.title = data["Title"]
- try:
- self.year = int(data["Year"])
- except (ValueError, TypeError):
- self.year = None
- self.content_rating = data["Rated"]
- self.genres = util.get_list(data["Genre"])
- self.genres_str = data["Genre"]
- try:
- self.imdb_rating = float(data["imdbRating"])
- except (ValueError, TypeError):
- self.imdb_rating = None
- try:
- self.imdb_votes = int(str(data["imdbVotes"]).replace(',', ''))
- except (ValueError, TypeError):
- self.imdb_votes = None
- try:
- self.metacritic_rating = int(data["Metascore"])
- except (ValueError, TypeError):
- self.metacritic_rating = None
- self.imdb_id = data["imdbID"]
- self.type = data["Type"]
- try:
- self.series_id = data["seriesID"]
- except (ValueError, TypeError, KeyError):
- self.series_id = None
- try:
- 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
+ def _parse(key, is_int=False, is_float=False, is_date=False, replace=None):
+ try:
+ value = str(data[key]).replace(replace, '') if replace else data[key]
+ if is_int:
+ return int(value)
+ elif is_float:
+ return float(value)
+ elif is_date:
+ return datetime.strptime(value, "%d %b %Y")
+ else:
+ return value
+ except (ValueError, TypeError, KeyError):
+ return None
+ self.title = _parse("Title")
+ self.year = _parse("Year", is_int=True)
+ self.released = _parse("Released", is_date=True)
+ self.content_rating = _parse("Rated")
+ self.genres_str = _parse("Genre")
+ self.genres = util.get_list(self.genres_str)
+ self.imdb_rating = _parse("imdbRating", is_float=True)
+ self.imdb_votes = _parse("imdbVotes", is_int=True, replace=",")
+ self.metacritic_rating = _parse("Metascore", is_int=True)
+ self.imdb_id = _parse("imdbID")
+ self.type = _parse("Type")
+ self.series_id = _parse("seriesID")
+ self.season_num = _parse("Season", is_int=True)
+ self.episode_num = _parse("Episode", is_int=True)
class OMDb:
diff --git a/modules/plex.py b/modules/plex.py
index 8d10349e..996e9865 100644
--- a/modules/plex.py
+++ b/modules/plex.py
@@ -793,6 +793,7 @@ class Plex(Library):
def get_filter_items(self, 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)
def get_collection_name_and_items(self, collection, smart_label_collection):
diff --git a/modules/sonarr.py b/modules/sonarr.py
index e1bc3c17..0195427b 100644
--- a/modules/sonarr.py
+++ b/modules/sonarr.py
@@ -110,7 +110,7 @@ class Sonarr:
invalid.extend(_i)
except ArrException as e:
logger.stacktrace()
- raise Failed(f"Radarr Error: {e}")
+ raise Failed(f"Sonarr Error: {e}")
for i, item in enumerate(tvdb_ids, 1):
path = item[1] if isinstance(item, tuple) else None
diff --git a/modules/tmdb.py b/modules/tmdb.py
index d47ac9ff..9be07e74 100644
--- a/modules/tmdb.py
+++ b/modules/tmdb.py
@@ -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_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:
def __init__(self, config, params):
self.config = config
self.apikey = params["apikey"]
self.language = params["language"]
+ self.expiration = params["expiration"]
logger.secret(self.apikey)
try:
self.TMDb = TMDbAPIs(self.apikey, language=self.language, session=self.config.session)
@@ -69,7 +158,7 @@ class TMDb:
raise Failed(f"TMDb Error: {e}")
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
if not check_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}")
else: return self.get_show(tmdb_id)
- def get_movie(self, tmdb_id, partial=None):
- try: return self.TMDb.movie(tmdb_id, partial=partial)
+ def get_movie(self, tmdb_id):
+ try: return TMDbMovie(self, tmdb_id)
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):
- try: return self.TMDb.tv_show(tmdb_id, partial=partial)
+ def get_show(self, tmdb_id):
+ try: return TMDbShow(self, tmdb_id)
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):
@@ -139,7 +228,7 @@ class TMDb:
except TMDbException as e: raise Failed(f"TMDb Error: No List found for TMDb ID {tmdb_id}: {e}")
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):
return self.TMDb.people_search(name)
@@ -219,7 +308,7 @@ class TMDb:
tmdb_name = collection.name
ids = [(t.id, "tmdb") for t in collection.movies]
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"))
else:
person = self.get_person(tmdb_id, partial="movie_credits,tv_credits")
diff --git a/modules/trakt.py b/modules/trakt.py
index d290013f..51ecd59c 100644
--- a/modules/trakt.py
+++ b/modules/trakt.py
@@ -241,7 +241,7 @@ class Trakt:
return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show")
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"):
values = util.get_list(trakt_lists, split=False)
diff --git a/modules/tvdb.py b/modules/tvdb.py
index 54a1cf21..bde31947 100644
--- a/modules/tvdb.py
+++ b/modules/tvdb.py
@@ -1,4 +1,5 @@
import requests, time
+from datetime import datetime
from lxml.etree import ParserError
from modules import util
from modules.util import Failed
@@ -66,11 +67,11 @@ class TVDbObj:
else:
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)
if len(parse_results) > 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):
place = "//div[@class='change_translation_text' and "
@@ -85,15 +86,17 @@ class TVDbObj:
if not self.title:
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]")
if self.is_movie:
- self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]")
- self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]")
- self.studios = parse_page("//strong[text()='Studio']/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()]", is_list=True)
+ 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:
- self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]")
- self.genres = parse_page("//strong[text()='Genres']/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.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
imdb_id = None
diff --git a/modules/util.py b/modules/util.py
index f872588e..5906fc79 100644
--- a/modules/util.py
+++ b/modules/util.py
@@ -111,7 +111,7 @@ def get_list(data, lower=False, upper=False, split=True, int_list=False):
elif int_list is True:
try: return [int(str(d).strip()) for d in list_data]
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):
int_values = []
@@ -438,12 +438,14 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa
else:
raise Failed(f"{error} Error: {display} {dict_data} is not a dictionary")
return final_list
- elif datatype in ["dict", "dictlist", "dictdict"]:
+ elif datatype in ["dict", "dictlist", "dictdict", "strdict"]:
if isinstance(value, dict):
if datatype == "dict":
return value
elif datatype == "dictlist":
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:
final_dict = {}
for dict_key, dict_data in value.items():
diff --git a/plex_meta_manager.py b/plex_meta_manager.py
index 2b183f91..c29f925e 100644
--- a/plex_meta_manager.py
+++ b/plex_meta_manager.py
@@ -2,7 +2,7 @@ import argparse, os, sys, time, traceback
from datetime import datetime
try:
- import plexapi, schedule
+ import plexapi, requests, schedule
from modules.logs import MyLogger
from plexapi.exceptions import NotFound
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
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"
def start(attrs):
@@ -129,6 +133,8 @@ def start(attrs):
logger.info_center("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")
logger.info_center(" |___/ ")
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']} "
elif "test" in attrs and attrs["test"]: start_type = "Test "
elif "collections" in attrs and attrs["collections"]: start_type = "Collections "
@@ -185,7 +191,10 @@ def start(attrs):
except Failed as e:
logger.stacktrace()
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()
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 Critic Rating Update: {library.mass_critic_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 Collection Mode Update: {library.mass_collection_mode}")
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"Genre Collections: {library.genre_collections}")
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"Item Operation: {library.items_library_operation}")
logger.debug("")
@@ -446,6 +458,11 @@ def library_operations(config, library):
sonarr_adds = []
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):
try:
library.reload(item)
@@ -471,6 +488,14 @@ def library_operations(config, library):
except Failed:
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])
if library.Radarr and library.radarr_add_all_existing and tmdb_id:
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))
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)
omdb_item = None
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 tmdb_id and not imdb_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}")
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:
try:
tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie)
@@ -514,9 +541,26 @@ def library_operations(config, library):
else:
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
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 tmdb_id and not imdb_id:
imdb_id = config.Convert.tmdb_to_imdb(tmdb_id)
@@ -533,8 +577,8 @@ def library_operations(config, library):
else:
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:
- tmdb_collections[tmdb_item.collection.id] = tmdb_item.collection.name
+ if library.tmdb_collections and tmdb_item and tmdb_item.collection_id:
+ tmdb_collections[tmdb_item.collection_id] = tmdb_item.collection_name
def get_rating(attribute):
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
elif mdb_item and attribute == "mdb_letterboxd":
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:
raise Failed
- if library.mass_genre_update:
+ if library.mass_genre_update or library.genre_mapper:
try:
- if tmdb_item and library.mass_genre_update == "tmdb":
- new_genres = [genre.name for genre in tmdb_item.genres]
- elif omdb_item and library.mass_genre_update == "omdb":
- new_genres = omdb_item.genres
- elif tvdb_item and library.mass_genre_update == "tvdb":
- new_genres = tvdb_item.genres
- else:
- raise Failed
+ new_genres = []
+ if library.mass_genre_update:
+ if tmdb_item and library.mass_genre_update == "tmdb":
+ new_genres = tmdb_item.genres
+ elif omdb_item and library.mass_genre_update == "omdb":
+ new_genres = omdb_item.genres
+ elif tvdb_item and library.mass_genre_update == "tvdb":
+ 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)
except Failed:
pass
@@ -595,35 +660,49 @@ def library_operations(config, library):
logger.info(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")
except Failed:
pass
- if library.mass_content_rating_update:
+ if library.mass_content_rating_update or library.content_rating_mapper:
try:
- if omdb_item and library.mass_content_rating_update == "omdb":
- new_rating = omdb_item.content_rating
- elif mdb_item and library.mass_content_rating_update == "mdb":
- new_rating = mdb_item.certification if mdb_item.certification else None
- elif mdb_item and library.mass_content_rating_update == "mdb_commonsense":
- new_rating = mdb_item.commonsense if mdb_item.commonsense else None
- else:
- raise Failed
- if new_rating is None:
- logger.info(f"{item.title[:25]:<25} | No Content Rating Found")
- elif str(item.rating) != str(new_rating):
+ new_rating = None
+ if library.mass_content_rating_update:
+ if omdb_item and library.mass_content_rating_update == "omdb":
+ new_rating = omdb_item.content_rating
+ elif mdb_item and library.mass_content_rating_update == "mdb":
+ new_rating = mdb_item.certification if mdb_item.certification else None
+ elif mdb_item and library.mass_content_rating_update == "mdb_commonsense":
+ new_rating = mdb_item.commonsense if mdb_item.commonsense else None
+ else:
+ raise Failed
+ 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})
logger.info(f"{item.title[:25]:<25} | Content Rating | {new_rating}")
except Failed:
pass
-
- if library.genre_mapper:
+ if library.mass_originally_available_update:
try:
- adds = []
- deletes = []
- library.reload(item)
- for genre in item.genres:
- if genre.tag in library.genre_mapper:
- deletes.append(genre.tag)
- if library.genre_mapper[genre.tag]:
- adds.append(library.genre_mapper[genre.tag])
- library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes)
+ if omdb_item and library.mass_originally_available_update == "omdb":
+ new_date = omdb_item.released
+ elif mdb_item and library.mass_originally_available_update == "mdb":
+ new_date = mdb_item.released
+ elif tvdb_item and library.mass_originally_available_update == "tvdb":
+ new_date = tvdb_item.released
+ elif tmdb_item and library.mass_originally_available_update == "tmdb":
+ new_date = tmdb_item.release_date if library.is_movie else tmdb_item.first_air_date
+ 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:
pass
@@ -737,7 +816,7 @@ def library_operations(config, library):
logger.info("")
logger.info(f"Metadata Backup Path: {library.metadata_backup['path']}")
logger.info("")
- meta = {}
+ meta = None
if os.path.exists(library.metadata_backup["path"]):
try:
meta, _, _ = yaml.util.load_yaml_guess_indent(open(library.metadata_backup["path"]))
@@ -749,6 +828,8 @@ def library_operations(config, library):
i += 1
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}")
+ if not meta:
+ meta = {}
if "metadata" not in meta:
meta["metadata"] = {}
items = library.get_all(load=True)
@@ -824,7 +905,6 @@ def run_collection(config, library, metadata, requested_collections):
items_added = 0
items_removed = 0
- valid = True
if not builder.smart_url and builder.builders and not builder.blank_collection:
logger.info("")
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()
library.stats["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):
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.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
if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection):
try: