#!/usr/bin/env python3 import re import sys import requests import argparse from pathlib import Path from difflib import SequenceMatcher TMDB_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJlOTIwMzJlMDk1NmEzMWEyZjQ4MDA4NGI0MTg4OTIyZCIsIm5iZiI6MTc5MDQzMjc5NS42NjcsInN1YiI6IjZhYjdkNjFiZDY5NmI1ZGFjNGM5NDRkMyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.IjddKtQ5X62A63kYslH24Pxg_TFCzu-QpGak_Pemgr8" if not TMDB_TOKEN: print("ERROR: TMDB_API_TOKEN is not set.") print() print("Example:") print(" export TMDB_API_TOKEN='your_token'") sys.exit(1) BASE_URL = "https://api.themoviedb.org/3" HEADERS = { "Authorization": f"Bearer {TMDB_TOKEN}", "accept": "application/json", } def tmdb_get(endpoint, params=None): """Perform a GET request against TMDb.""" url = BASE_URL + endpoint response = requests.get( url, headers=HEADERS, params=params, timeout=15, ) response.raise_for_status() return response.json() def find_by_title(title, media_type="tv", year=None, language="de-DE"): """ Search TMDB by title. media_type: tv | movie """ params = { "query": title, "language": language, } if year: if media_type == "tv": params["first_air_date_year"] = year else: params["year"] = year return tmdb_get( f"/search/{media_type}", params, ) def find_by_tmdb_id(tmdb_id, language="de-DE"): """ Try to determine whether a TMDB ID is a TV show or movie. Returns: ("tv", data) ("movie", data) (None, None) """ try: data = get_tv_details(tmdb_id, language) return "tv", data except requests.HTTPError: pass try: data = get_movie_details(tmdb_id, language) return "movie", data except requests.HTTPError: pass return None, None def get_tv_details(tmdb_id , language="de-DE"): """ Retrieve TV show details in German. """ return tmdb_get( f"/tv/{tmdb_id}", { "language": language, }, ) def get_movie_details(tmdb_id, language="de-DE"): """ Retrieve movie details in German. """ return tmdb_get( f"/movie/{tmdb_id}", { "language": language, }, ) def get_season(tmdb_id, season_number): return tmdb_get( f"/tv/{tmdb_id}/season/{season_number}", { "language": "de-DE", }, ) def get_episode_list(show): #_, show = find_by_tmdb_id(tmdb_id) episodes = [] for season in show.get("seasons", []): season_number = season["season_number"] # skip specials if desired if season_number == 0: continue season_data = get_season(show.get("id"), season_number) for episode in season_data.get("episodes", []): episodes.append({ "season": season_number, "episode": episode["episode_number"], "title": episode["name"], "air_date": episode["air_date"], "runtime": episode.get("runtime"), "tmdb_id": episode["id"], }) return episodes def print_results(result): media_type, data = result print(f"Type : {media_type}") print(f"TMDB ID : {data.get('id')}") print(f"Title : {data.get('name') or data.get('title')}") print(f"Original : {data.get('original_name') or data.get('original_title')}") print(f"Year : {(data.get('first_air_date') or data.get('release_date') or '')[:4]}") print(f"Rating : {data.get('vote_average')}") print(f"Votes : {data.get('vote_count')}") print(f"Status : {data.get('status')}") print(f"Tagline : {data.get('tagline')}") print(f"Overview : {data.get('overview')}") genres = ", ".join(g["name"] for g in data.get("genres", [])) print(f"Genres : {genres}") print(f"Poster : https://image.tmdb.org/t/p/original{data.get('poster_path', '')}") print(f"Backdrop : https://image.tmdb.org/t/p/original{data.get('backdrop_path', '')}") if media_type == "tv": print(f"Seasons : {data.get('number_of_seasons')}") print(f"Episodes : {data.get('number_of_episodes')}") def normalize(text, title, year): text = text.lower() # remove series title text = re.sub(re.escape(title.lower()), " ", text) # remove year text = re.sub(rf"\b{year}\b", " ", text) # remove anything in [] including the brackets text = re.sub(r"\[[^\]]*\]", " ", text) # remove punctuation text = re.sub(r"[^\w\s]", " ", text) # remove episode numbers like S01E01 text = re.sub(r"\bs\d+e\d+\b", " ", text, flags=re.I) # collapse spaces text = re.sub(r"\s+", " ", text).strip() return text def best_episode_match(filename, episodes,title,year): filename_norm = normalize(Path(filename).stem, title, year) best = None best_score = 0 for ep in episodes: score = SequenceMatcher( None, filename_norm, normalize(ep["title"], title, year) ).ratio() if score > best_score: best_score = score best = ep return best, best_score def main(): parser = argparse.ArgumentParser() parser.add_argument("series", help='Series name, e.g. "Maya the Bee (1975) [10620]"') args = parser.parse_args() if len(sys.argv) != 2 or not Path(args.series).is_dir(): print("Usage:") print() print(" python tmdb_lookup.py ") print(" Seires Folder needs to be this Style: Maya the Bee (1975) [10620]") sys.exit(1) series_path = Path(args.series) series = series_path.name m = re.match(r"^(.*?)\s*\((\d{4})\)\s*\[(\d+)\]$", series) if not m: parser.error(f"Invalid series format: {args.series}") file_title = m.group(1).strip() file_year = int(m.group(2)) file_tmdb_id = int(m.group(3)) print(f"TITLE={file_title}") print(f"YEAR={file_year}") print(f"TMDB_ID={file_tmdb_id}") if file_tmdb_id == 0: print("Tmdb ID required") exit(1) print(f"Looking up IMDb ID: {file_tmdb_id}") data = find_by_tmdb_id(file_tmdb_id) media_type, media_data = data print_results(data) episodes = get_episode_list(media_data) episode_map = { (ep["season"], ep["episode"]): ep for ep in episodes } for file in series_path.rglob("*"): m = re.search(r"S(\d{2})E(\d{2})", file.name, re.I) if not m: m_b = re.search(r"Season (\d{2})", file.name, re.I) if m_b: continue print(f"No season/episode number found: {file.name}") continue season = int(m.group(1)) episode= int(m.group(2)) ep = episode_map.get((season, episode)) file_list = [] new_file_list = [] if ep: m = re.match( r"^(.*?S\d{2}E\d{2})\s*-\s*(.*?)\s*(\[[^\]]+\].*)?$", file.stem, re.I, ) if m: prefix = m.group(1) # Andor (2022) - S01E07 quality = m.group(3) or "" # [WEBRip-1080p][AAC 5.1][h265] new_name = ( f"{prefix} - {ep['title']}" f"{(' ' + quality) if quality else ''}" f"{file.suffix}" ) print( f"{file.name} -> " f"{new_name}" ) file_list.append(file) new_file_list.append(new_name) else: print( f"Episod* not found: " f"S{season:02d}E{episode:02d}" ) title=(f"{(data.get('name') or data.get('title'))} " f"({(data.get('first_air_date') or data.get('release_date') or '')[:4]})") print(f"{series} -> {title}") answer = input("Rename files? [y/N]: ").strip().lower() if answer in ("y", "yes"): print("Proceeding...") file_list.append(series_path) new_file_list.append(title) for file, new_file in zip(file_list,new_file_list): file.rename(file.with_name(new_file)) else: print("Cancelled.") if __name__ == "__main__": main()