diff --git a/README.md b/README.md index c0f408f..d98213a 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,38 @@ https://anilist.co/anime/99263/Tate-no-Yuusha-no-Nariagari - You can remove any existing entries from the example file as they are purely instructional - Upon startup it will check if the file is a valid YAML file. The most likely reason it's not is because you didn't put quotes around an anime title with special characters (e.g. ":") in it. +#### Duplicate Plex Titles + +For Plex shows and movies with the same title, use the optional `guid` field in the mapping. + +For example, both of these Rurouni Kenshin shows are shown as "Rurouni Kenshin" in Plex: + +```yaml + - title: "Rurouni Kenshin" + guid: plex://show/5d9c07ece264b7001fc38094 + seasons: + - season: 1 + anilist-id: 45 + - season: 2 + anilist-id: 45 + - season: 3 + anilist-id: 45 + + - title: "Rurouni Kenshin (2023)" + guid: plex://show/6330a57e9705fab2b34f656d + seasons: + - season: 1 + anilist-id: 142877 +``` + +When the `guid` field is set, the `title` and `synonyms` fields are ignored. However, you should still use different titles for human readability. + +To find the guid, perform the following steps: +1. Open this URL in an **incognito browser window**: https://app.plex.tv/desktop/#!/search?pivot=top&query= +2. Search for your series or movie and click on the correct entry +3. Copy everything after `metadata%2F`, so for https://app.plex.tv/desktop/#!/provider/tv.plex.provider.discover/details?key=%2Flibrary%2Fmetadata%2F6330a57e9705fab2b34f656d copy `6330a57e9705fab2b34f656d` +4. If it's a TV show, add `plex://show/` before that identifier, for movies use `plex://movie/` + #### Community mappings There are some mappings provided by the Github community at https://github.com/RickDB/PlexAniSync-Custom-Mappings/. You can use them by specifying `remote-urls` like in the example mapping file. diff --git a/plexanisync/anilist.py b/plexanisync/anilist.py index 07a8373..7e7414e 100644 --- a/plexanisync/anilist.py +++ b/plexanisync/anilist.py @@ -50,6 +50,7 @@ def match_to_plex(self, anilist_series: List[AnilistSeries], plex_series_watched plex_title = plex_series.title plex_title_sort = plex_series.title_sort plex_title_original = plex_series.title_original + plex_guid = plex_series.guid plex_year = plex_series.year plex_seasons = plex_series.seasons plex_show_rating = plex_series.rating @@ -65,7 +66,7 @@ def match_to_plex(self, anilist_series: List[AnilistSeries], plex_series_watched for plex_season in plex_seasons: season_mappings: List[AnilistCustomMapping] = self.__retrieve_season_mappings( - plex_title, plex_season.season_number + plex_title, plex_guid, plex_season.season_number ) # split season -> handle it in "any remaining seasons" section if season_mappings and len(season_mappings) == 1: @@ -171,7 +172,7 @@ def match_to_plex(self, anilist_series: List[AnilistSeries], plex_series_watched ] potential_titles = list(potential_titles_cleaned) - season_mappings = self.__retrieve_season_mappings(plex_title, season_number) + season_mappings = self.__retrieve_season_mappings(plex_title, plex_guid, season_number) # Custom mapping check - check user list if season_mappings: watchcounts = self.__map_watchcount_to_seasons(plex_title, season_mappings, plex_season.watched_episodes) @@ -262,7 +263,7 @@ def match_to_plex(self, anilist_series: List[AnilistSeries], plex_series_watched media_id_search = None # ignore the Plex year since Plex does not have years for seasons skip_year_check = True - season_mappings = self.__retrieve_season_mappings(plex_title, season_number) + season_mappings = self.__retrieve_season_mappings(plex_title, plex_guid, season_number) if season_mappings: watchcounts = self.__map_watchcount_to_seasons(plex_title, season_mappings, plex_season.watched_episodes) @@ -651,14 +652,17 @@ def __update_episode_incremental( for current_episodes_watched in range(anilist_episodes_watched + 1, watched_episode_count + 1): self.graphql.update_series(series.anilist_id, current_episodes_watched, new_status, plex_rating) - def __retrieve_season_mappings(self, title: str, season: int) -> List[AnilistCustomMapping]: + def __retrieve_season_mappings(self, title: str, guid: str, season: int) -> List[AnilistCustomMapping]: season_mappings: List[AnilistCustomMapping] = [] - if self.custom_mappings and title.lower() in self.custom_mappings: - season_mappings = self.custom_mappings[title.lower()] - # filter mappings by season - season_mappings = [e for e in season_mappings if e.season == season] + if self.custom_mappings: + if guid in self.custom_mappings: + season_mappings = self.custom_mappings[guid] + elif title.lower() in self.custom_mappings: + season_mappings = self.custom_mappings[title.lower()] + # filter mappings by season + season_mappings = [e for e in season_mappings if e.season == season] return season_mappings def __map_watchcount_to_seasons( diff --git a/plexanisync/custom_mappings.py b/plexanisync/custom_mappings.py index b387a78..aac6529 100644 --- a/plexanisync/custom_mappings.py +++ b/plexanisync/custom_mappings.py @@ -77,6 +77,7 @@ def construct_scalar(self, node): def read_custom_mappings() -> Dict[str, List[AnilistCustomMapping]]: custom_mappings: Dict[str, List[AnilistCustomMapping]] = {} + title_guid_mappings: Dict[str, str] = {} if not os.path.isfile(MAPPING_FILE): logger.info(f"Custom map file not found: {MAPPING_FILE}") return custom_mappings @@ -112,9 +113,9 @@ def read_custom_mappings() -> Dict[str, List[AnilistCustomMapping]]: logger.error(f'Custom Mappings {mapping_location} validation failed!') __handle_yaml_error(file_mappings_remote, e) - __add_mappings(custom_mappings, mapping_location, file_mappings_remote) + __add_mappings(custom_mappings, title_guid_mappings, mapping_location, file_mappings_remote) - __add_mappings(custom_mappings, MAPPING_FILE, file_mappings_local) + __add_mappings(custom_mappings, title_guid_mappings, MAPPING_FILE, file_mappings_local) return custom_mappings @@ -137,12 +138,15 @@ def __handle_yaml_error(file_mappings_local, error): sys.exit(1) -def __add_mappings(custom_mappings, mapping_location, file_mappings): +def __add_mappings(custom_mappings: Dict[str, List[AnilistCustomMapping]], + title_guid_mappings: Dict[str, str], + mapping_location, file_mappings): # handles missing and empty 'entries' entries = file_mappings.get('entries', []) or [] for file_entry in entries: series_title = str(file_entry['title']) synonyms: List[str] = file_entry.get('synonyms', []) + guid: str = str(file_entry.get('guid', "")) series_mappings: List[AnilistCustomMapping] = [] for file_season in file_entry['seasons']: season = file_season['season'] @@ -155,12 +159,24 @@ def __add_mappings(custom_mappings, mapping_location, file_mappings): series_mappings.append(AnilistCustomMapping(season, anilist_id, start)) if synonyms: logger.debug(f"{series_title} has synonyms: {synonyms}") + + if guid: + # store the mapping under the guid if one is set + custom_mappings[guid] = series_mappings + for title in [series_title] + synonyms: title_lower = title.lower() if title_lower in custom_mappings: logger.info(f"Overwriting previous mapping for {title}") + if title_lower in title_guid_mappings and not guid: + # if the current mapping doesn't have a guid, remove the guid mapping with the same title + # this ensures that users can override community mappings without specifying the guid field + custom_mappings.pop(title_guid_mappings[title_lower], None) custom_mappings[title_lower] = series_mappings + if guid: + title_guid_mappings[title_lower] = guid + # Get the custom mappings from the web. def __get_custom_mapping_remote(file_mappings) -> List[Tuple[str, str]]: diff --git a/plexanisync/plexmodule.py b/plexanisync/plexmodule.py index 552f02a..de4d5b1 100644 --- a/plexanisync/plexmodule.py +++ b/plexanisync/plexmodule.py @@ -32,6 +32,7 @@ class PlexWatchedSeries: title: str title_sort: str title_original: str + guid: str year: int seasons: List[PlexSeason] anilist_id: Optional[int] @@ -220,6 +221,7 @@ def get_watched_shows(self, shows: List[Show]) -> Optional[List[PlexWatchedSerie show.title.strip(), show.titleSort.strip(), show.originalTitle.strip(), + show.guid, year, seasons, anilist_id, @@ -259,6 +261,7 @@ def get_watched_shows(self, shows: List[Show]) -> Optional[List[PlexWatchedSerie show.title.strip(), show.titleSort.strip(), show.originalTitle.strip(), + show.guid, year, [PlexSeason(1, rating, 1, 1, 1)], anilist_id,