dbr / tvnamer

Automatic TV episode file renamer, uses data from thetvdb.com via tvdb_api
https://pypi.python.org/pypi/tvnamer/
The Unlicense
907 stars 115 forks source link

Way to rename based off of episode name? #211

Open csm10495 opened 2 years ago

csm10495 commented 2 years ago

Hey folks,

Say I have a series that is always named like

SXEY - <Episode Title>.mp4

so for example:

S1E2 - A Good Night.mp4

Would be season 1, episode 2 titled "A Good Night".

Now the issue i have is that the season and episode are almost certainly wrong, BUT the episode name is correct (or as close as it can be based of of weird characters, etc.).

Is there a way to convince tvnamer to rename to the correct Season/Episode just based off the title (and of course I'd give it the tvdbid or name via command line args to disambiguate what show it is)?

Thanks!

csm10495 commented 2 years ago

Upon further investigation this is really similar to #193 except in my case i have an incorrect season/episode instead of a correct series name.

nephlm commented 2 years ago

My reading of the relevant code suggests that a season/episode combo or date is required. And since your season/episode is unreliable I don't think you could include it in the regex without leading tvnamer astray. Even if you managed to pass that test you need to have a series name in the regex, but that's somewhat easier to fix if you know what series these all came from.

erichlf commented 1 month ago

I haven't used this in a long time, but it used to work. I believe the reason I haven't used it recently is because you now need an api key and I was being lazy.

#!/usr/bin/env python3

import os
import sys
import re
import string
from tvdb_api import Tvdb
from titlecase import titlecase

log = open('log.txt', 'w')
global autoRename
autoRename = False  # This is used to auto-rename

def get_show_name(path):
    #use the known file structure to determine the show name.
    show = re.sub('^\/.+\/Series/?','',path)

    return re.sub(r'\.',r' ',show)

def process_file(video):
    #Use regex to determine season, episode, and name of episode.

    season = None
    episode = None
    name = None

    try:
        matches = re.search('^S(\d+)E(\d+)\.(.+)\.S(\d+)E(\d+)\.(.+)\.\w{3}$',
                              video)
        season = [int(matches.group(1)), int(matches.group(4))]
        episode = [int(matches.group(2)), int(matches.group(5))]
        name = [matches.group(3), matches.group(6)]
        name = [re.sub(r'\.',r' ', name[0]), re.sub(r'\.',r' ', name[1])]
        name = [re.sub(r'([-&])',r' \1 ', name[0]),
                re.sub(r'([-&])',r' \1 ', name[1])]
        name = [re.sub(r',',r', ', name[0]), re.sub(r',',r', ', name[1])]
    except:
        matches = re.search('^S(\d+)E(\d+)\.(.+)\.\w{3}$',video)
        try:
            season = int(matches.group(1))
        except:
            print("Couldn't determine the season for {}.".format(video))
            sys.exit(1)
        try:
            episode = int(matches.group(2))
        except:
            print("Couldn't determine the episode for {}.".format(video))
            sys.exit(1)
        try:
            name = matches.group(3)
        except:
            print("Couldn't determine the episode name for {}.".format(video))
            sys.exit(1)

        name = re.sub(r'\.',r' ', name)
        name = re.sub(r'\s?([-&])\s',r' \1 ', name)
        name = re.sub(r',',r', ', name)
        name = re.sub(r'([MD]r|Mrs|Ms)\s', r'\1. ', name)

    return (season, episode, name)

def is_correct(show, season, episode, name):
    #check if the given episode has the correct season and episode numbers

    series = show['seriesname']
    if isinstance(name, list):
        print('Found {} - Season {:02d} Episode {:02d} {}'.format(series,
                                                                  season[0],
                                                                  episode[0],
                                                                  name[0]))
        print('\tSeason {:02d} Episode {:02d} {} '.format(series,
                                                          season[1],
                                                          episode[1],
                                                          name[1]))
        print('Checking if it has the correct season and episode numbers.')
        ep1 = check_episode(series, show, season[0], episode[0], name[0])
        ep2 = check_episode(series, show, season[1], episode[1], name[1])
        if ep1 is None and ep2 is None:
            return None
        elif ep1 is None:
            ep1 = 'S{:02d}E{:02d}.{}'.format(season[0], episode[0], name[0])
            ep1 = re.sub(r', ', r',', ep1)
            ep1 = re.sub(r' ([-&]) ', r'\1', ep1)
            ep1 = re.sub(r' ', r'.', ep1)
        elif ep2 is None:
            ep2 = 'S{:02d}E{:02d}.{}'.format(season[1], episode[1], name[1])
            ep2 = re.sub(r', ', r',', ep2)
            ep2 = re.sub(r' ([-&]) ', r'\1', ep2)
            ep2 = re.sub(r' ', r'.', ep2)
        return ep1 + '.' + ep2
    else:
        print('Found {} - Season {:02d} Episode {:02d} {}'.format(series, season, episode, name))
        print('Checking if it has the correct season and episode numbers.')
        return check_episode(series, show, season, episode, name)

def check_episode(series, show, season, episode, name):
    (seasonNumber, episodeNumber, episodeName) = find_episode(show, name)
    if seasonNumber and episodeNumber and episodeName:
        if not (season == seasonNumber and episode == episodeNumber):
            print("Either the season or the episode didn't match.")
            print('Found {} - S{:02d}E{:02d} {} instead.'.format(series,
                                                                 seasonNumber,
                                                                 episodeNumber,
                                                                 episodeName))
            episodeName = re.sub(r', ', r',', episodeName)
            episodeName = re.sub(r' ([-&]) ', r'\1', episodeName)
            episodeName = re.sub(r' ', r'.', episodeName)
            return ('S{:02d}E{:02d}.{}'.format(seasonNumber, episodeNumber,
                                               episodeName))
        elif name != episodeName: #This will force things to titlecase
            episodeName = re.sub(r', ', r',', episodeName)
            episodeName = re.sub(r' ([-&]) ', r'\1', episodeName)
            episodeName = re.sub(r' ', r'.', episodeName)
            return ('S{:02d}E{:02d}.{}'.format(seasonNumber, episodeNumber,
                                               episodeName))
        else: #season and episode numbers were correct
            print('The season and episode are correct. Moving on.')
            return None
    else: #Couldn't find an episode with this name.
        return None

def find_episode(show, name):
    #Find the season and episode numbers from the given episode name.

    result = show.search(name, key='episodeName')
    if result:
        return process_result(result, name)
    else: #try searching for name without space around -
        tmp = re.sub(r' (-) ',r'\1',name)
        tmp = re.sub(r' (:) ',r'\1 ',tmp)
        result = show.search(tmp, key='episodeName')
        if result:
            return process_result(result, tmp)
        else:
            tmp = re.sub(r'([MD]r|Mrs|Ms)\. ',r'\1 ', name)
            result = show.search(tmp, key='episodeName')
            if result:
                name = tmp
                return process_result(result, name)

    if not result:
        print('Couldn\'t find an episode with this name.')
        log.write('{} - {}\n'.format(show['seriesname'], name))
        return (None,None,None)

def process_result(result, name):
    for episode in result:
        if name.lower() == episode['episodename'].lower():
            seasonNumber = int(episode['seasonnumber'])
            episodeNumber = int(episode['episodenumber'])
            episodeName = titlecase(episode['episodename'])
            return (seasonNumber, episodeNumber, episodeName)
    print('No episode with this name appears to exist for this series.')
    alphabet = string.ascii_lowercase
    i = 0
    s = ''
    for episode in result:
        i += 1
        epStr = ('S{:02d}E{:02d}'.format(int(episode['seasonnumber']),
                                    int(episode['episodenumber'])))
        s += ('({}) {} - {}\n'.format(str(i),epStr,episode['episodename']))

    s += ('({}) None of the above.'.format(str(i+1)))
    lst = [str(j) for j in range(1,i+2)]
    ans = ask('Which episode would you like?\n' + s, lst, str(i+1))
    if ans == str(i+1):
        print('No episode selected. Skipping this episode.')
        return (None,None,None)
    else:
        ans = int(ans) - 1 #convert back to list index
        seasonNumber = int(result[ans]['seasonnumber'])
        episodeNumber = int(result[ans]['episodenumber'])
        episodeName = titlecase(result[ans]['episodename'])
        response = ask('Use the new episode name?', ['y', 'n'], 'y')
        if response == 'y':
            print('Using new episode name.')
            return (seasonNumber, episodeNumber, episodeName)
        else:
            print('Using old episode name.')
            return (seasonNumber, episodeNumber, name)

    return (None,None,None)

def move_file(f1, f2, path):
    global autoRename
    if f1 != f2:
        response = ask('Move {} to {}?\nPath is {}'.format(f1,f2,path), ['y','n','a','q'],
                    'y')
        if response == 'a':
            autoRename = True
            response = 'y'
        if response == 'y' or autoRename:
            print('Renaming file.')
            source = ('{}/{}'.format(path,f1))
            destination = ('{}/{}'.format(path,f2))
            os.rename(source,destination)
        elif response == 'q':
            print('Quitting')
            raise SystemExit

def ask(question, options, default = "y"):
    """
    Takes a question (string), list of options and a default value (used
    when user simply hits enter).
    Asks until valid option is entered.
    """
    # Highlight default option with [ ]
    options_str = []
    for x in options:
        if x == default:
            x = "[{}]".format(x)
        if x != '':
            options_str.append(x)
    options_str = "/".join(options_str)

    while True:
        print(question)
        print("({})".format(options_str))
        try:
            ans = input().strip()
        except KeyboardInterrupt:
            print("\n", sys.exc_info())
            raise UserAbort(sys.exc_info())

        if ans in options:
            return ans
        elif ans == '':
            return default

def usage():
#   print usage
    print('Usage: ./SeriesRenamer.py <optional directory>')

def main(args):

    if len(args) == 1:
        top_directory = args[0]
    else:
        usage()
        return 1
    tvdb = Tvdb()

    for path, dirnames, filenames in os.walk(top_directory):
        show = get_show_name(path)
        if not show: #skip if can't find show
            continue

        series = show
        show = tvdb[show]
        for video in filenames:
            #get the original extension
            ext = os.path.splitext(video)[-1]
            print('\nProcessing file: {}'.format(video))

            #only process video files
            if not (ext.lower() in ('.vob','.avi', '.mp4', '.mpg', '.mkv', '.flv')):
                continue

            (season, episode, name) = process_file(video)
            if name:
                newFile = is_correct(show, season, episode, name)
                if newFile:
                    newFile += ext #tack our extension back on
                    print(newFile)
                    move_file(video, newFile, path)
            else:
                    print('Couldn\'t determine the episode name.')

    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))