diff --git a/README.md b/README.md index 0f138f7..d5efde7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ Duplicate the config.sample.json file to a file named *config.json* and set the * `tvpath`: Root folder where the organized tv episodes will go. * `ext`: A list of valid media file extensions to easily filter out none media related files. * `log_path`: Path to a folder to output all renaming done to keep track and check for any errors and safe csv files. -* `movie_db_api`: Register and get your themoviedb.com **API Key (v3 auth)** acces from [here](https://www.themoviedb.org/settings/api). +* `movie_db_api`: Register and get your themoviedb.com **API Key (v3 auth)** acces from [here](https://www.themoviedb.org/settings/api). +* `min_file_size`: Minimal filesize to be considered a relevant media file in bytes. #### Emby integration * `emby_url`: url where your emby instance is reachable diff --git a/config.sample.json b/config.sample.json index 658fe0e..701acf5 100644 --- a/config.sample.json +++ b/config.sample.json @@ -7,7 +7,8 @@ "tvpath": "/media/movie/movie/tv", "ext": ["mp4", "mkv", "avi", "m4v"], "log_folder": "/home/user/logs/media_organize", - "movie_db_api": "aaaabbbbccccdddd1111222233333444" + "movie_db_api": "aaaabbbbccccdddd1111222233333444", + "min_file_size": 50000000 }, "emby": { "emby_url": "http://media.local:8096/emby", diff --git a/interface.py b/interface.py index a113985..c5cd2f7 100755 --- a/interface.py +++ b/interface.py @@ -34,10 +34,9 @@ def get_config(): def get_pending_all(config): """ figure out what needs to be done """ - movie_downpath = config['media']['movie_downpath'] tv_downpath = config['media']['tv_downpath'] # call subfunction to collect pending - pending_movie = moviesort.get_pending(movie_downpath) + pending_movie = moviesort.MovieHandler().pending pending_tv = tvsort.get_pending(tv_downpath) pending_trailer = len(trailers.get_pending(config)) pending_movie_fix = len(id_fix.get_pending(config)) @@ -91,13 +90,13 @@ def print_menu(stdscr, current_row_idx, menu, pending): def sel_handler(menu_item, config): """ lunch scripts from here based on selection """ if menu_item == 'All': - moviesort.main(config) + moviesort.main() tvsort.main(config, tvsort_id) db_export.main(config) trailers.main(config) id_fix.main(config) elif menu_item == 'Movies': - moviesort.main(config) + moviesort.main() elif menu_item == 'TV shows': tvsort.main(config, tvsort_id) elif menu_item == 'DB export': diff --git a/src/moviesort.py b/src/moviesort.py index 142d9a9..6210e98 100644 --- a/src/moviesort.py +++ b/src/moviesort.py @@ -3,261 +3,239 @@ import logging import os import re -import requests import subprocess -from time import sleep + +import requests + +from interface import get_config -def get_pending(movie_downpath): - """ - return how many movies are pending - return 0 when nothing to do - """ - pending = len(os.listdir(movie_downpath)) - return pending +class MovieHandler(): + """ handler for moving files around """ + CONFIG = get_config() -def move_to_sort(movie_downpath, sortpath, ext): - """ moves movies to sortpath """ - for dirpath, _, filenames in os.walk(movie_downpath): - for filename in filenames: - path = os.path.join(dirpath, filename) - _, extension = os.path.splitext(path) - extension = extension.lstrip('.').lower() - f_size = os.stat(path).st_size - # TODO: set f_size in config.json - if extension in ext and 'sample' not in filename and f_size > 50000000: - move_to = os.path.join(sortpath, filename) - os.rename(path, move_to) - pending = os.listdir(sortpath) - return pending + def __init__(self): + """ check for pending movie files """ + self.pending = self.get_pending() + def get_pending(self): + """ + return how many movies are pending + return 0 when nothing to do + """ + movie_downpath = self.CONFIG['media']['movie_downpath'] + pending = len(os.listdir(movie_downpath)) + return pending -def split_filename(filename): - """ split the filename into moviename, year, file_ext """ - print(filename) - file_ext = os.path.splitext(filename)[1] - year_id_pattern = re.compile(r'\d{4}') - year_list = year_id_pattern.findall(filename) - # remove clear false - for year in year_list: - if year == '1080': - # there were no movies back in 1080 - year_list.remove(year) - file_split = filename.split(year) - if len(file_split[0]) == 0: - year_list.remove(year) - if len(year_list) != 1: - print('year extraction failed for: ' + filename) - year = input('whats the year?\n') - else: - year = year_list[0] - moviename = filename.split(year)[0].rstrip('.') - return moviename, year, file_ext + def move_to_sort(self): + """ moving files from movie_downpath to sortpath """ + # read out config + sortpath = self.CONFIG['media']['sortpath'] + movie_downpath = self.CONFIG['media']['movie_downpath'] + min_file_size = self.CONFIG['media']['min_file_size'] + ext = self.CONFIG['media']['ext'] + for dirpath, _, filenames in os.walk(movie_downpath): + for filename in filenames: + path = os.path.join(dirpath, filename) + _, extension = os.path.splitext(path) + extension = extension.lstrip('.').lower() + f_size = os.stat(path).st_size + if (extension in ext and + 'sample' not in filename and + f_size > min_file_size): + move_to = os.path.join(sortpath, filename) + os.rename(path, move_to) + pending = os.listdir(sortpath) + return pending - -def get_results(movie_db_api, moviename, year = None): - """ return results from api call """ - moviename_encoded = moviename.lower().replace("its", "")\ - .replace(" ", "%20").replace(".", "%20").replace("'", "%20") - # call api with year passed or not - if year: - request = requests.get('https://api.themoviedb.org/3/search/movie?api_key=' + - movie_db_api + '&query=' + moviename_encoded + '&year=' + - year + '&language=en-US&include_adult=false').json() - else: - request = requests.get('https://api.themoviedb.org/3/search/movie?api_key=' + - movie_db_api + '&query=' + moviename_encoded + - '&language=en-US&include_adult=false').json() - try: - results = len(request['results']) - except KeyError: - results = 0 - if results == 0: - # try again without last word of string - moviename_encoded = '%20'.join(moviename_encoded.split('%20')[0:-1]) - request = requests.get('https://api.themoviedb.org/3/search/movie?api_key=' + - movie_db_api + '&query=' + moviename_encoded + '&year=' + - year + '&language=en-US&include_adult=false').json() - results = len(request['results']) - # return - return results, request - - -def search_for(movie_db_api, moviename, filename, year = None): - """ - takes the moviename and year from the filename - and returns clean name and year - """ - # get results from API - results, request = get_results(movie_db_api, moviename, year) - # clear when only one result - if results == 1: - selection = 0 - # select for more than 0 result - elif results > 0: - short_list = [] - long_list = [] - counter = 0 - for item in request['results']: - movie_title = item['title'] - movie_date = item['release_date'] - movie_year = movie_date.split('-')[0] - movie_desc = item['overview'] - short_list_str = f'[{str(counter)}] {movie_title} - {movie_year}' - long_list_str = f'{short_list}\n{movie_desc}' - short_list.append(short_list_str) - long_list.append(long_list_str) - counter = counter + 1 - short_list.append('[?] show more') - # print short menu - print('\nfilename: ' + filename) - for line in short_list: - print(line) - selection = input('select input: ') - # print long menu - if selection == '?': - for line in long_list: - print(line) - selection = input('select input: ') - # no result, try again - else: - return None, None - # get and return title and year - movie_title = request['results'][int(selection)]['title'] - movie_year = request['results'][int(selection)]['release_date'].split('-')[0] - return movie_title, movie_year - - -def movie_rename(sortpath, movie_db_api): - """ renames the movie file """ - to_rename = os.listdir(sortpath) - # os.chdir(sortpath) - for filename in to_rename: - # split up - moviename, year, file_ext = split_filename(filename) - # try to figure things out - while True: - # first try - movie_title, movie_year = search_for(movie_db_api, moviename, filename, year, ) - if movie_title and movie_year: - break - # second try with - 1 year - movie_title, movie_year = search_for(movie_db_api, moviename, filename, str(int(year) - 1), ) - if movie_title and movie_year: - break - # third try with + 1 year - movie_title, movie_year = search_for(movie_db_api, moviename, filename, str(int(year) + 1), ) - if movie_title and movie_year: - break - # last try without year - movie_title, movie_year = search_for(movie_db_api, moviename, filename) - if movie_title and movie_year: - break - # manual overwrite - print(filename + '\nNo result found, search again with:') - moviename = input('movie name: ') - year = input('year: ') - movie_title, movie_year = search_for(moviename, year, filename) - break - if not movie_title or not movie_year: - # last check - return False - else: - # clean invalid chars - movie_title = movie_title.replace('/', '-') - # do it - rename_to = movie_title + ' (' + movie_year + ')' + file_ext - old_file = os.path.join(sortpath, filename) - new_file = os.path.join(sortpath, rename_to) + def rename_files(self, identified): + """ apply the identified filenames and rename """ + sortpath = self.CONFIG['media']['sortpath'] + renamed = [] + for movie in identified: + old_file = os.path.join(sortpath, movie.filename) + new_file = os.path.join(sortpath, movie.new_filename) os.rename(old_file, new_file) - logging.info('movie:from [{}] to [{}]'.format(filename,rename_to)) - return True + logging.info( + 'movie:from [%s] to [%s]', movie.filename, movie.new_filename + ) + renamed.append(movie.filename) + return renamed - -def move_to_archive(sortpath, moviepath): - """ moves renamed movie to archive, - returns list for further processing """ - new_movies = [] - to_move = os.listdir(sortpath) - if to_move: - print() - for i in to_move: - print(i) - _ = input('\ncontinue?') - to_move = os.listdir(sortpath) - for movie in to_move: - movie_name = os.path.splitext(movie)[0] - year_pattern = re.compile(r'(\()([0-9]{4})(\))') - year = year_pattern.findall(movie_name)[0][1] - old_file = os.path.join(sortpath, movie) - new_folder = os.path.join(moviepath, year, movie_name) - new_file = os.path.join(new_folder, movie) + def move_to_archive(self, identified): + """ move renamed filed to archive """ + sortpath = self.CONFIG['media']['sortpath'] + moviepath = self.CONFIG['media']['moviepath'] + # confirm + print('\nrenamed:') + for movie in identified: + print(f'from: {movie.filename} \nto: {movie.new_filename}\n') + to_continue = input('\ncontinue? Y/n') + if to_continue == 'n': + print('cancle...') + return False + moved = [] + for movie in identified: + old_file = os.path.join(sortpath, movie.new_filename) + new_folder = os.path.join( + moviepath, str(movie.year), movie.new_moviename + ) + new_file = os.path.join(new_folder, movie.new_filename) try: os.makedirs(new_folder) except FileExistsError: - print(f'{movie_name}\nalready exists in archive') + print(f'{movie.new_filename}\nalready exists in archive') double = input('[o]: overwrite, [s]: skip and ignore\n') if double == 'o': subprocess.call(["trash", new_folder]) os.makedirs(new_folder) elif double == 's': continue - else: - pass - finally: - pass os.rename(old_file, new_file) - new_movies.append(movie_name) - return new_movies + moved.append(movie.new_filename) + return len(moved) + + def cleanup(self, moved): + """ clean up movie_downpath and sortpath folder """ + sortpath = self.CONFIG['media']['sortpath'] + movie_downpath = self.CONFIG['media']['movie_downpath'] + if moved: + # moved without errors + to_clean_list = os.listdir(movie_downpath) + for to_clean in to_clean_list: + to_trash = os.path.join(movie_downpath, to_clean) + subprocess.call(["trash", to_trash]) + to_clean_list = os.listdir(sortpath) + for to_clean in to_clean_list: + to_trash = os.path.join(sortpath, to_clean) + subprocess.call(["trash", to_trash]) + else: + # failed to rename + move_back = os.listdir(sortpath) + for movie_pending in move_back: + old_path = os.path.join(sortpath, movie_pending) + new_path = os.path.join(movie_downpath, movie_pending) + os.rename(old_path, new_path) -# clean up -def cleanup(movie_downpath, sortpath, renamed): - """ cleans up the movie_downpath folder """ - if renamed: - # renamed without errors - to_clean_list = os.listdir(movie_downpath) - for to_clean in to_clean_list: - to_trash = os.path.join(movie_downpath, to_clean) - subprocess.call(["trash", to_trash]) - to_clean_list = os.listdir(sortpath) - for to_clean in to_clean_list: - to_trash = os.path.join(sortpath, to_clean) - subprocess.call(["trash", to_trash]) - else: - # failed to rename - move_back = os.listdir(sortpath) - for movie in move_back: - old_path = os.path.join(sortpath, movie) - new_path = os.path.join(movie_downpath, movie) - os.rename(old_path, new_path) +class MovieIdentify(): + """ describes and identifies a single movie """ + + CONFIG = get_config() + + def __init__(self, filename): + """ parse filename """ + self.filename = filename + self.moviename, self.year, self.file_ext = self.split_filename() + self.moviename_encoded = self.encode_moviename() + self.new_moviename, self.new_filename = self.get_new_filename() + + def split_filename(self): + """ build raw values from filename """ + file_ext = os.path.splitext(self.filename)[1] + year_id_pattern = re.compile(r'\d{4}') + year_list = year_id_pattern.findall(self.filename) + # remove clear false + for year in year_list: + if year == '1080': + # there were no movies back in 1080 + year_list.remove(year) + file_split = self.filename.split(year) + if len(file_split[0]) == 0: + year_list.remove(year) + if len(year_list) != 1: + print('year extraction failed for:\n' + self.filename) + year = input('whats the year?\n') + else: + year = year_list[0] + moviename = self.filename.split(year)[0].rstrip('.') + return moviename, int(year), file_ext + + def encode_moviename(self): + """ url encode and clean the moviename """ + encoded = self.moviename.lower().replace(' ', '%20') + encoded = encoded.replace('.', '%20').replace("'", '%20') + return encoded + + def get_results(self): + """ get all possible matches """ + movie_db_api = self.CONFIG['media']['movie_db_api'] + year_file = self.year + # try +/- one year + year_list = [year_file, year_file + 1, year_file - 1] + for year in year_list: + url = ( + 'https://api.themoviedb.org/3/search/movie?' + + f'api_key={movie_db_api}&query={self.moviename_encoded}' + + f'&year={year}&language=en-US&include_adult=false' + ) + request = requests.get(url).json() + results = request['results'] + # stop if found + if results: + break + return results + + def pick_result(self, results): + """ select best possible match from list of results """ + if len(results) == 1: + selection = 0 + elif len(results) > 1: + short_list = [] + long_list = [] + counter = 0 + for item in results: + nr = str(counter) + movie_title = item['title'] + movie_date = item['release_date'] + movie_year = movie_date.split('-')[0] + movie_desc = item['overview'] + short_list_str = f'[{nr}] {movie_title} - {movie_year}' + long_list_str = f'{short_list_str}\n{movie_desc}' + short_list.append(short_list_str) + long_list.append(long_list_str) + counter = counter + 1 + short_list.append('[?] show more') + # print short menu + print('\nfilename: ' + self.filename) + for line in short_list: + print(line) + selection = input('select input: ') + # print long menu + if selection == '?': + for line in long_list: + print(line) + selection = input('select input: ') + return int(selection) + + def get_new_filename(self): + """ get the new filename """ + results = self.get_results() + selection = self.pick_result(results) + result = results[selection] + # build new_filename + year_dedected = result['release_date'].split('-')[0] + name_dedected = result['title'] + new_moviename = f'{name_dedected} ({year_dedected})' + new_filename = f'{new_moviename}{self.file_ext}' + return new_moviename, new_filename -def main(config): - """ main to sort movies """ - # read config - movie_downpath = config['media']['movie_downpath'] - sortpath = config['media']['sortpath'] - moviepath = config['media']['moviepath'] - ext = config['media']['ext'] - movie_db_api = config['media']['movie_db_api'] +def main(): + """ main to lunch moviesort """ + handler = MovieHandler() # check if pending - pending = get_pending(movie_downpath) - if not pending: - print('no movies to sort') - sleep(2) + if not handler.pending: return - - # move to sort folder - pending = move_to_sort(movie_downpath, sortpath, ext) - if not pending: - print('no movies to sort') - sleep(2) - return - - movie_renamed = movie_rename(sortpath, movie_db_api) - if movie_renamed: - renamed = move_to_archive(sortpath, moviepath) - # clean folders - cleanup(movie_downpath, sortpath, renamed) + to_rename = handler.move_to_sort() + # identify + identified = [] + for i in to_rename: + movie = MovieIdentify(i) + identified.append(movie) + # rename and move + renamed = handler.rename_files(identified) + if renamed: + moved = handler.move_to_archive(identified) + handler.cleanup(moved)