#!/usr/bin/env python3 # pylint: disable=C0103,W0621 # pip install tmdbv3api import os import re import subprocess import sys import typing import tmdbv3api # TODO Override files without warning # TODO Dry run mode (just comment the last line ^^) # Typing Episode = typing.Any # TODO # Constants API_KEY_PASS_PATH = "http/themoviedb.org" VIDEO_EXTENSIONS = {"mp4", "mkv", "avi", "webm"} # Functions def get_pass_data(path: str) -> typing.Dict[str, str]: """ Returns the data stored in the Unix password manager given its path. """ run = subprocess.run(["pass", path], stdout=subprocess.PIPE, check=True) lines = run.stdout.decode().split("\n") data = dict() data["pass"] = lines[0] for line in lines[1:]: match = re.match(r"(\w+): ?(.+)", line) if match: data[match[1]] = match[2] return data def confirm(text: str) -> bool: res = input(text + " [yn] ") while res not in ("y", "n"): res = input("Please answer with y or n: ") return res == "y" def episode_identifier(episode: typing.Any) -> str: return ( f"S{episode['season_number']:02d}E" + f"{episode['episode_number']:02d} {episode['name']}" ) dryrun = "-n" in sys.argv if dryrun: dryrun = True sys.argv.remove("-n") # Connecting to TMBDB tmdb = tmdbv3api.TMDb() tmdb.api_key = get_pass_data(API_KEY_PASS_PATH)["api"] tmdb.language = sys.argv[1] # Searching the TV show name (by current directory name) tv = tmdbv3api.TV() season = tmdbv3api.Season() if len(sys.argv) >= 3: show_name = sys.argv[2] else: show_name = os.path.split(os.path.realpath(os.path.curdir))[1] if "(" in show_name: show_name = show_name.split("(")[0].strip() search = tv.search(show_name) # Asking the user to select the one show = None for res in search: print(f"#{res.id} {res.name} ({res.first_air_date[:4]}): {res.overview}") if confirm("Is this the show for this folder?"): show = tv.details(res.id) break if not show: print("Could not find a matching " + f"show on TheMovieDatabase for {show_name}.") sys.exit(1) # Retrieving all the episode of the show episodes: typing.List[Episode] = list() print(f"List of episodes for {show.name}:") for season_number in range(0, show.number_of_seasons + 1): season_details = season.details(show.id, season_number) try: season_details.episodes except AttributeError: continue for episode in season_details.episodes: episodes.append(episode) print(f"- {episode_identifier(episode)}") # Finding movie files in the folder print("List of video files in this folder") videos: typing.List[typing.Tuple[str, str]] = list() for root, dirs, files in os.walk(os.path.curdir): for filename in files: basename, ext = os.path.splitext(filename) real_ext = ext[1:].lower() if real_ext not in VIDEO_EXTENSIONS: continue videos.append((root, filename)) print(f"- {filename}") def get_episode(season_number: int, episode_number: int) -> typing.Optional[Episode]: # TODO Make more efficient using indexing for episode in episodes: if ( episode["season_number"] == season_number and episode["episode_number"] == episode_number ): return episode return None # Matching movie files to episode associations: typing.List[typing.Tuple[typing.Tuple[str, str], Episode]] = list() for video in videos: root, filename = video match = re.search(r"S(\d+)E(\d+)", filename) print(f"Treating file: {root}/{filename}") episode = None season_number = 0 episode_number = 0 while not episode: if match: season_number = int(match[1]) episode_number = int(match[2]) else: try: season_number = int(input("Season number ?")) episode_number = int(input("Episode number ?")) except ValueError: continue if season_number < 0 and episode_number < 0: break match = None episode = get_episode(season_number, episode_number) if not episode: print( f" could not find episode S{season_number:02d}E{episode_number:02d} in TMBD" ) # Skip if not episode: if season_number < -1 and episode_number < -1: # Skip all break # Skip one continue associations.append((video, episode)) print(f" associated to: {episode_identifier(episode)}") # Rename video files for association in associations: video, episode = association root, filename = video basename, ext = os.path.splitext(filename) new_name = f"{show.name} ({show.first_air_date[:4]}) {episode_identifier(episode)}" # Rename all file with the same base name as the original file so we # can rename nfo files and subtitles (only one though) for a_filename in os.listdir(root): a_basename, a_ext = os.path.splitext(a_filename) if a_basename == basename: old_path = os.path.join(root, a_filename) new_path = os.path.join(root, new_name + a_ext) if old_path == new_path: continue print(old_path, "->", new_path) if not dryrun: os.rename(old_path, new_path)