diff --git a/.github/workflows/validate-tvdb-seasons.yml b/.github/workflows/validate-tvdb-seasons.yml new file mode 100644 index 0000000..cfbeec9 --- /dev/null +++ b/.github/workflows/validate-tvdb-seasons.yml @@ -0,0 +1,48 @@ +name: Validate TVDB Seasons +on: + pull_request_target: + branches: + - main + paths: + - "series-tvdb.en.yaml" + - "**.py" +env: + PYTHON_VERSION: "3.10" +jobs: + validate-added-seasons-against-tvdb: + runs-on: ubuntu-latest + steps: + # TODO: maybe make this step a pre-requisite to any CI flow + - name: Prevent file change + uses: xalvarez/prevent-file-change-action@v1 + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + pattern: .*\.py + trustedAuthors: evie-lau,Soitora,reconman + allowNewFiles: false + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Cache python env + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: ${{ env.pythonLocation }}-${{ hashFiles('validate-tvdb-seasons.py') }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tvdb_v4_official python-dotenv pyyaml deepdiff + - name: Run season validation against TVDB + env: + TVDB_API_KEY: ${{ secrets.TVDB_API_KEY }} + run: python validate-tvdb-seasons.py + - name: Create updated entries list + uses: actions/upload-artifact@v4 + with: + path: temp.yaml \ No newline at end of file diff --git a/series-tmdb.en.yaml b/series-tmdb.en.yaml index cafd2df..782056a 100644 --- a/series-tmdb.en.yaml +++ b/series-tmdb.en.yaml @@ -2331,6 +2331,15 @@ entries: - season: 1 anilist-id: 150972 + - title: "SAKAMOTO DAYS" + guid: "plex://show/62ea662444a4745bb2f3a8ce" + # imdb: https://www.imdb.com/title/tt17069148/ + # tmdb: https://www.themoviedb.org/tv/207332 + # tvdb: https://www.thetvdb.com/dereferrer/series/423732 + seasons: + - season: 1 + anilist-id: 177709 + - title: "Scott Pilgrim Takes Off" guid: plex://show/61dd97c364b2acfab575e1c3 # imdb: https://www.imdb.com/title/tt16969708/ @@ -3175,6 +3184,21 @@ entries: - season: 5 anilist-id: 142481 + - title: "Yu Yu Hakusho" + guid: "plex://show/5d9c0813705e7a001e6d1afb" + # imdb: https://www.imdb.com/title/tt0185133/ + # tmdb: https://www.themoviedb.org/tv/30669 + # tvdb: https://www.thetvdb.com/dereferrer/series/76665 + seasons: + - season: 1 + anilist-id: 392 + - season: 2 + anilist-id: 392 + - season: 3 + anilist-id: 392 + - season: 4 + anilist-id: 392 + - title: "Yu-Gi-Oh! Duel Monsters" guid: plex://show/5d9c081dba6eb9001fb9c35c # imdb: https://www.imdb.com/title/tt0247902/ @@ -3218,3 +3242,12 @@ entries: - season: 1 anilist-id: 159831 + - title: "Übel Blatt" + guid: "plex://show/65db309639d5eabf6d1e207b" + # imdb: https://www.imdb.com/title/tt32581027/ + # tmdb: https://www.themoviedb.org/tv/247367 + # tvdb: https://www.thetvdb.com/dereferrer/series/446485 + seasons: + - season: 1 + anilist-id: 175198 + diff --git a/series-tvdb.en.yaml b/series-tvdb.en.yaml index b0f19de..92c808b 100644 --- a/series-tvdb.en.yaml +++ b/series-tvdb.en.yaml @@ -738,7 +738,7 @@ entries: - season: 9 anilist-id: 813 - - title: "Drop Kick on My Devil!!" + - title: "Dropkick on My Devil!!" guid: plex://show/5d9c090502391c001f59412a seasons: - season: 1 @@ -2375,6 +2375,15 @@ entries: - season: 1 anilist-id: 150972 + - title: "SAKAMOTO DAYS" + guid: "plex://show/62ea662444a4745bb2f3a8ce" + # imdb: https://www.imdb.com/title/tt17069148/ + # tmdb: https://www.themoviedb.org/tv/207332 + # tvdb: https://www.thetvdb.com/dereferrer/series/423732 + seasons: + - season: 1 + anilist-id: 177709 + - title: "Scott Pilgrim Takes Off" guid: plex://show/61dd97c364b2acfab575e1c3 # imdb: https://www.imdb.com/title/tt16969708/ @@ -3231,6 +3240,21 @@ entries: - season: 5 anilist-id: 142481 + - title: "Yu Yu Hakusho" + guid: "plex://show/5d9c0813705e7a001e6d1afb" + # imdb: https://www.imdb.com/title/tt0185133/ + # tmdb: https://www.themoviedb.org/tv/30669 + # tvdb: https://www.thetvdb.com/dereferrer/series/76665 + seasons: + - season: 1 + anilist-id: 392 + - season: 2 + anilist-id: 392 + - season: 3 + anilist-id: 392 + - season: 4 + anilist-id: 392 + - title: "Yu-Gi-Oh! Duel Monsters" guid: plex://show/5d9c081dba6eb9001fb9c35c # imdb: https://www.imdb.com/title/tt0247902/ @@ -3282,3 +3306,12 @@ entries: - season: 1 anilist-id: 159831 + - title: "Übel Blatt" + guid: "plex://show/65db309639d5eabf6d1e207b" + # imdb: https://www.imdb.com/title/tt32581027/ + # tmdb: https://www.themoviedb.org/tv/247367 + # tvdb: https://www.thetvdb.com/dereferrer/series/446485 + seasons: + - season: 1 + anilist-id: 175198 + diff --git a/validate-tvdb-seasons.py b/validate-tvdb-seasons.py new file mode 100644 index 0000000..28c819c --- /dev/null +++ b/validate-tvdb-seasons.py @@ -0,0 +1,130 @@ +import os, sys, subprocess +import tvdb_v4_official, yaml +from dotenv import load_dotenv +from deepdiff import DeepDiff + + +# Get TVDB ID and series information for a show +def getTvdbEntry(showName): + # Get TVDB ID of show + showId = None + series = None + searchResults = tvdb.search(showName, type="series") + for result in searchResults: + if (resultMatchesShow(result, showName)): + # print(result) + showId = result['tvdb_id'] + series = tvdb.get_series_extended(showId) + # only mark as found if it's an Anime show + if matchesAnimeCriteria(series): + break + else: # reset stored values if not a match + showId = None + series = None + return showId, series + + +# Check if showName matches anything result name, aliases, or translation names +def resultMatchesShow(result, showName): + # TODO: need to normalize some characters, such as dashes and apostrophes. Use unicodedata.normalize. Test with "KonoSuba – God's blessing on this wonderful world!!" (curly vs straight apostrophe) + return ((showName == result['name']) or + ('aliases' in result and showName in result['aliases']) or + ('translations' in result and showName in result['translations'].values())) + + +# Match for AniList anime criteria (arbitrary): genre=[Anime,Animation], country=[jpn,kor,chn,twn] +validGenres = {'Anime','Animation'} +validCountries = {'jpn','kor','chn','twn'} +def matchesAnimeCriteria(series): + seriesGenreSet = {item['name'] for item in series['genres']} + return ((seriesGenreSet.intersection(validGenres)) and + (series['originalCountry'] in validCountries)) + + +# Validate user-mapped season entries against TVDB seasons +def validateShowSeasons(showName, seasonsToFind): + errors = 0 + + showId, series = getTvdbEntry(showName) + print("Validating: " + showName + " [" + str(showId) + "] - Seasons " + str(seasonsToFind)) + + if (showId is None): + print("\t[WARNING] No TVDB series result: " + showName) + return errors + # TODO: does not work for primary_type: movie, maybe separate method for those? Test with 5cm per second + tvdbSeasons = [season['number'] for season in series['seasons'] if season['type']['type'] == 'official'] + + # print("Found show: " + showName + " (" + showId + "), with seasons: " + str(tvdbSeasons)) + # print("Validating user-mapped seasons: " + str(seasonsToFind)) + invalidSeasons = [] + for s in seasonsToFind: + if s not in tvdbSeasons: + errors += 1 + invalidSeasons.append(s) + + if errors > 0: + print("\t[ERROR] Did not find season(s): " + str(invalidSeasons) + " in show: " + showName) + return errors + + +# Parse temp.yaml and validate shows/seasons against TVDB +def validateMappings(file="temp.yaml"): + errors = 0 + with open(file, mode='r') as f: + mappings = yaml.safe_load(f) + for show in sorted(mappings['entries'], key=lambda entry: (entry['title'], entry['seasons'])): + showName = show['title'] + seasons = set([s['season'] for s in show['seasons'] if 'season' in s]) + errors += validateShowSeasons(showName, seasons) + return errors + + +# Finds new and changed entries and writes them into a temp yaml file +def extractNewMappings(): + old_mappings = dict() + new_mappings = dict() + + # get old version of TVDB file + # TODO read old_mappings YAML from stdout so the file is not needed anymore + with open('tvdb-old.yaml', mode='w') as f: + subprocess.run( + ['git', 'show', 'origin/main:series-tvdb.en.yaml'], + text=True, + check=True, + stdout=f + ) + + with open('tvdb-old.yaml', mode='r') as f: + old_mappings = yaml.safe_load(f) + + with open('series-tvdb.en.yaml', mode='r') as f: + new_mappings = yaml.safe_load(f) + + # create new mapping file with just the changed entries + diff = DeepDiff(old_mappings['entries'], new_mappings['entries'], ignore_order=True) + change_groups = { "entries": [] } + # diff.affected_root_keys contains the indices of the changed entries + for key in diff.affected_root_keys: + change_groups['entries'].append(new_mappings['entries'][key]) + + # write the changed entries to temp.yaml + with open('temp.yaml', mode='w') as file: + yaml.dump(change_groups, file, encoding='utf-8', allow_unicode=True) + + +def cleanup(): + os.remove("tvdb-old.yaml") + os.remove("temp.yaml") + + +# TODO: cross reference anilist-id show name +extractNewMappings() +# Load API Key and initialize tvdb +load_dotenv() +apikey = os.getenv("TVDB_API_KEY") +tvdb = tvdb_v4_official.TVDB(apikey) + +errors = validateMappings() +if errors != 0: + sys.exit("Found " + str(errors) + " error(s) in the season mappings") +cleanup() \ No newline at end of file