diff --git a/VERSION b/VERSION index 2f6d98f8..73519de3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop20 +1.16.1-develop21 diff --git a/docs/config/operations.md b/docs/config/operations.md index e2674e2c..d845d5eb 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -16,24 +16,25 @@ 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_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) | ## Genre Mapper 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/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/builder.py b/modules/builder.py index eaf58f1c..d8fe56f0 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -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"): @@ -1326,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) diff --git a/modules/config.py b/modules/config.py index 4289b148..5afb4db6 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,9 +31,9 @@ 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_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata"} +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"} +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"} mass_rating_options = { "tmdb": "Use TMDb Rating", "omdb": "Use IMDb Rating through OMDb", @@ -45,7 +45,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: @@ -447,7 +449,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...") diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 7cdfca87..73879294 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -448,6 +448,10 @@ 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 = {} + for k, v in library.anidb_map.values(): + reverse_anidb[v] = k + for i, item in enumerate(items, 1): try: library.reload(item) @@ -518,6 +522,16 @@ 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: + try: + anidb_item = config.AniDB.get_anime(reverse_anidb[item.ratingKey]) + except Failed as e: + logger.error(str(e)) + else: + logger.info(f"{item.title[:25]:<25} | No AniDB ID for Guid: {item.guid}") + 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_originally_available_update == "mdb": @@ -563,6 +577,10 @@ 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 @@ -574,6 +592,8 @@ def library_operations(config, library): 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 library.edit_tags("genre", item, sync_tags=new_genres) @@ -622,10 +642,12 @@ def library_operations(config, library): 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_content_rating_update == "tvdb": + elif tvdb_item and library.mass_originally_available_update == "tvdb": new_date = tvdb_item.released - elif tmdb_item and library.mass_content_rating_update == "tvdb": + 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: