From 89a0fcf4622ee57c11f4086b30433c544952c770 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 27 May 2021 16:01:04 +0700 Subject: [PATCH 1/7] adding exception handeling for request errors to api --- src/db_export.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/db_export.py b/src/db_export.py index 8e70e0d..c3cf883 100644 --- a/src/db_export.py +++ b/src/db_export.py @@ -1,6 +1,7 @@ """ export collection from emby to CSV """ import csv +from time import sleep from os import path import requests @@ -27,8 +28,12 @@ class DatabaseExport(): '&fields=Genres,MediaStreams,Overview,' 'ProviderIds,Path,RunTimeTicks' '&SortBy=DateCreated&SortOrder=Descending') + try: + response = requests.get(url) + except requests.exceptions.ConnectionError: + sleep(5) + response = requests.get(url) - response = requests.get(url) all_movies = response.json()['Items'] # episodes url = (f'{emby_url}/Users/{emby_user_id}/Items?api_key={emby_api_key}' @@ -36,8 +41,12 @@ class DatabaseExport(): '&Fields=DateCreated,Genres,MediaStreams,' 'MediaSources,Overview,ProviderIds,Path,RunTimeTicks' '&SortBy=DateCreated&SortOrder=Descending&IsMissing=false') + try: + response = requests.get(url) + except requests.exceptions.ConnectionError: + sleep(5) + response = requests.get(url) - response = requests.get(url) all_episodes = response.json()['Items'] return all_movies, all_episodes From b610926821d5fed6a71f3adc1166f740263c88a2 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 29 May 2021 17:30:36 +0700 Subject: [PATCH 2/7] rewrote interface.py in it's own class, extended functionality to refresh pending within curses --- interface.py | 264 +++++++++++++++++++++++++++++---------------------- 1 file changed, 150 insertions(+), 114 deletions(-) diff --git a/interface.py b/interface.py index f87c0d1..5196109 100755 --- a/interface.py +++ b/interface.py @@ -15,132 +15,168 @@ import src.trailers as trailers import src.id_fix as id_fix -def get_pending_all(): - """ figure out what needs to be done """ - # call subfunction to collect pending - pending_movie = moviesort.MovieHandler().pending - pending_tv = tvsort.TvHandler().pending - pending_trailer = len(trailers.TrailerHandler().pending) - pending_movie_fix = len(id_fix.MovieNameFix().pending) - pending_total = pending_movie + pending_tv + pending_trailer + pending_movie_fix - # build dict - pending = {} - pending['movies'] = pending_movie - pending['tv'] = pending_tv - pending['trailer'] = pending_trailer - pending['movie_fix'] = pending_movie_fix - pending['total'] = pending_total - return pending +class Interface(): + """ creating and removing the menu """ + CONFIG = get_config() + log_folder = CONFIG['media']['log_folder'] + log_file = path.join(log_folder, 'rename.log') + logging.basicConfig( + filename=log_file,level=logging.INFO,format='%(asctime)s:%(message)s' + ) -def print_menu(stdscr, current_row_idx, menu, pending): - """ print menu with populated pending count """ - # build stdscr - h, w = stdscr.getmaxyx() - longest = len(max(menu)) - x = w // 2 - longest - stdscr.clear() - # loop through menu items - for idx, row in enumerate(menu): - # menu items count - if row == 'All': - pending_count = pending['total'] - elif row == 'Movies': - pending_count = pending['movies'] - elif row == 'TV shows': - pending_count = pending['tv'] - elif row == 'Trailer download': - pending_count = pending['trailer'] - elif row == 'Fix Movie Names': - pending_count = pending['movie_fix'] + def __init__(self): + self.menu = self.build_menu() + self.stdscr = None + self.menu_item = 0 + self.pending = self.get_pending_all() + + def get_pending_all(self): + """ figure out what needs to be done """ + # call subfunction to collect pending + pending = {} + pending_movie = moviesort.MovieHandler().pending + pending_tv = tvsort.TvHandler().pending + pending['movies'] = pending_movie + pending['tv'] = pending_tv + # based on config key + if 'emby' in self.CONFIG.keys(): + pending_trailer = len(trailers.TrailerHandler().pending) + pending_movie_fix = len(id_fix.MovieNameFix().pending) + pending['trailer'] = pending_trailer + pending['movie_fix'] = pending_movie_fix + pending_total = (pending_movie + pending_tv + + pending_trailer + pending_movie_fix) else: - pending_count = ' ' - # center whole - y = h // 2 - len(menu) + idx - # print string to menu - text = f'[{pending_count}] {row}' - if idx == current_row_idx: - stdscr.attron(curses.color_pair(1)) - stdscr.addstr(y, x, text) - stdscr.attroff(curses.color_pair(1)) - else: - stdscr.addstr(y, x, text) - # load - stdscr.refresh() + pending_total = pending_movie + pending_tv + pending['total'] = pending_total + return pending + def build_menu(self): + """ build the menu based on availabe keys in config file """ + menu = ['All', 'Movies', 'TV shows', 'Trailer download', + 'Fix Movie Names', 'DB export','Exit'] + config_keys = self.CONFIG.keys() + if not 'emby' in config_keys: + menu.remove('Fix Movie Names') + menu.remove('DB export') + if not 'ydl_opts' in config_keys: + menu.remove('Trailer download') + return menu -def sel_handler(menu_item): - """ lunch scripts from here based on selection """ - if menu_item == 'All': - moviesort.main() - tvsort.main() - db_export.main() - trailers.main() - id_fix.main() - elif menu_item == 'Movies': - moviesort.main() - elif menu_item == 'TV shows': - tvsort.main() - elif menu_item == 'DB export': - db_export.main() - elif menu_item == 'Trailer download': - trailers.main() - elif menu_item == 'Fix Movie Names': - id_fix.main() + def create_interface(self): + """ create the main loop for curses.wrapper """ + while True: + menu_item = curses.wrapper(self.curses_main) + if menu_item != 'Exit': + self.sel_handler() + sleep(3) + else: + return - -def curses_main(stdscr, menu): - """ curses main to desplay and restart the menu """ - curses.curs_set(0) - curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_WHITE) - current_row_idx = 0 - pending = get_pending_all() - print_menu(stdscr, current_row_idx, menu, pending) - # endless loop - while True: - # wait for exit signal - try: - key = stdscr.getch() - stdscr.clear() - # react to kee press - if key == curses.KEY_UP and current_row_idx > 0: - current_row_idx -= 1 - elif key == curses.KEY_DOWN and current_row_idx < len(menu) - 1: - current_row_idx += 1 - elif key == curses.KEY_ENTER or key in [10, 13]: - menu_item = menu[current_row_idx] - stdscr.addstr(0, 0, f'start task: {menu_item}') + def curses_main(self, stdscr): + """ curses main to desplay and restart the menu """ + self.stdscr = stdscr + curses.curs_set(0) + curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_WHITE) + current_row_idx = 0 + self.print_menu(current_row_idx) + # endless loop + while True: + # wait for exit signal + try: + key = stdscr.getch() + stdscr.clear() + # react to kee press + last = len(self.menu) - 1 + if key == curses.KEY_UP and current_row_idx > 0: + current_row_idx -= 1 + elif key == curses.KEY_DOWN and current_row_idx < last: + current_row_idx += 1 + elif key == curses.KEY_ENTER or key in [10, 13]: + menu_item = self.menu[current_row_idx] + stdscr.addstr(0, 0, f'start task: {menu_item}') + stdscr.refresh() + sleep(1) + # exit curses and do something + return menu_item + elif key == ord('q'): + return 'Exit' + elif key == ord('r'): + stdscr.addstr(0, 0, 'refreshing pending') + self.pending = self.get_pending_all() + stdscr.refresh() + sleep(1) + # print + self.print_menu(current_row_idx) stdscr.refresh() - sleep(1) - # exit curses and do something - return menu_item - # print - print_menu(stdscr, current_row_idx, menu, pending) - stdscr.refresh() - except KeyboardInterrupt: - # clean exit on ctrl + c - return 'Exit' + except KeyboardInterrupt: + # clean exit on ctrl + c + return 'Exit' + + def sel_handler(self): + """ lunch scripts from here based on selection """ + if self.menu_item == 'All': + moviesort.main() + tvsort.main() + trailers.main() + id_fix.main() + db_export.main() + elif self.menu_item == 'Movies': + moviesort.main() + elif self.menu_item == 'TV shows': + tvsort.main() + elif self.menu_item == 'Trailer download': + trailers.main() + elif self.menu_item == 'Fix Movie Names': + id_fix.main() + elif self.menu_item == 'DB export': + db_export.main() + + def print_menu(self, current_row_idx): + """ print menu with populated pending count """ + # build stdscr + menu = self.menu + h, w = self.stdscr.getmaxyx() + longest = len(max(menu)) + x = w // 2 - longest + self.stdscr.clear() + # help + self.stdscr.addstr(h - 1, x, 'q: quit, r: refresh') + # loop through menu items + for idx, row in enumerate(menu): + # menu items count + if row == 'All': + pending_count = self.pending['total'] + elif row == 'Movies': + pending_count = self.pending['movies'] + elif row == 'TV shows': + pending_count = self.pending['tv'] + elif row == 'Trailer download': + pending_count = self.pending['trailer'] + elif row == 'Fix Movie Names': + pending_count = self.pending['movie_fix'] + else: + pending_count = ' ' + # center whole + y = h // 2 - len(menu) + idx + # print string to menu + text = f'[{pending_count}] {row}' + if idx == current_row_idx: + self.stdscr.attron(curses.color_pair(1)) + self.stdscr.addstr(y, x, text) + self.stdscr.attroff(curses.color_pair(1)) + else: + self.stdscr.addstr(y, x, text) + # load + self.stdscr.refresh() def main(): """ main wraps the curses menu """ # setup - menu = ['All', 'Movies', 'TV shows', 'DB export', 'Trailer download', 'Fix Movie Names', 'Exit'] - config = get_config() - log_folder = config['media']['log_folder'] - log_file = path.join(log_folder, 'rename.log') - logging.basicConfig(filename=log_file,level=logging.INFO,format='%(asctime)s:%(message)s') - # endless loop - while True: - pending = get_pending_all() - if not pending: - return - menu_item = curses.wrapper(curses_main, menu) - if menu_item == 'Exit': - return - else: - sel_handler(menu_item) - sleep(3) + window = Interface() + window.create_interface() # start here From adb94d7435c69165ab3d8137f30543554f213b7d Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 29 May 2021 17:46:33 +0700 Subject: [PATCH 3/7] converted sel_handler to staticmethod to avoid delay for curses refresh --- interface.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/interface.py b/interface.py index 5196109..62eaa70 100755 --- a/interface.py +++ b/interface.py @@ -22,7 +22,7 @@ class Interface(): log_folder = CONFIG['media']['log_folder'] log_file = path.join(log_folder, 'rename.log') logging.basicConfig( - filename=log_file,level=logging.INFO,format='%(asctime)s:%(message)s' + filename=log_file, level=logging.INFO, format='%(asctime)s:%(message)s' ) def __init__(self): @@ -45,8 +45,8 @@ class Interface(): pending_movie_fix = len(id_fix.MovieNameFix().pending) pending['trailer'] = pending_trailer pending['movie_fix'] = pending_movie_fix - pending_total = (pending_movie + pending_tv + - pending_trailer + pending_movie_fix) + pending_total = (pending_movie + pending_tv + + pending_trailer + pending_movie_fix) else: pending_total = pending_movie + pending_tv pending['total'] = pending_total @@ -55,12 +55,12 @@ class Interface(): def build_menu(self): """ build the menu based on availabe keys in config file """ menu = ['All', 'Movies', 'TV shows', 'Trailer download', - 'Fix Movie Names', 'DB export','Exit'] + 'Fix Movie Names', 'DB export', 'Exit'] config_keys = self.CONFIG.keys() - if not 'emby' in config_keys: + if 'emby' not in config_keys: menu.remove('Fix Movie Names') menu.remove('DB export') - if not 'ydl_opts' in config_keys: + if 'ydl_opts' not in config_keys: menu.remove('Trailer download') return menu @@ -69,7 +69,7 @@ class Interface(): while True: menu_item = curses.wrapper(self.curses_main) if menu_item != 'Exit': - self.sel_handler() + self.sel_handler(menu_item) sleep(3) else: return @@ -114,23 +114,24 @@ class Interface(): # clean exit on ctrl + c return 'Exit' - def sel_handler(self): + @staticmethod + def sel_handler(menu_item): """ lunch scripts from here based on selection """ - if self.menu_item == 'All': + if menu_item == 'All': moviesort.main() tvsort.main() trailers.main() id_fix.main() db_export.main() - elif self.menu_item == 'Movies': + elif menu_item == 'Movies': moviesort.main() - elif self.menu_item == 'TV shows': + elif menu_item == 'TV shows': tvsort.main() - elif self.menu_item == 'Trailer download': + elif menu_item == 'Trailer download': trailers.main() - elif self.menu_item == 'Fix Movie Names': + elif menu_item == 'Fix Movie Names': id_fix.main() - elif self.menu_item == 'DB export': + elif menu_item == 'DB export': db_export.main() def print_menu(self, current_row_idx): From 5f69b22efef809a61ec1019b2dab8d8e1747f634 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 29 May 2021 17:48:54 +0700 Subject: [PATCH 4/7] autorefresh pending on completed task --- interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/interface.py b/interface.py index 62eaa70..3f5ecfe 100755 --- a/interface.py +++ b/interface.py @@ -71,6 +71,7 @@ class Interface(): if menu_item != 'Exit': self.sel_handler(menu_item) sleep(3) + self.pending = self.get_pending_all() else: return From cbe6a4d77f79d77050f4184f1c3f7777a4a1349c Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 29 May 2021 17:56:34 +0700 Subject: [PATCH 5/7] shortening logoutput to just new_file_name --- src/tvsort.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tvsort.py b/src/tvsort.py index 508a3be..207f4cf 100644 --- a/src/tvsort.py +++ b/src/tvsort.py @@ -356,7 +356,9 @@ class TvHandler(): os.rename(old_file, new_file) # finish up renamed.append(new_file) - logging.info('tv:from [%s] to [%s]', episode.filename, new_file) + logging.info( + 'tv:from [%s] to [%s]', episode.filename, new_file_name + ) return renamed def move_to_archive(self): From 90bfbc57bb287ccd3ae62336eaac190a5311cf5f Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 30 May 2021 12:17:16 +0700 Subject: [PATCH 6/7] improved menu setup and updated documentation --- README.md | 25 ++++++++++++++++--------- interface.py | 20 ++++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d5efde7..1fc1ada 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ This project is used and tested under Linux and is ideal to be used from something like a Raspberry Pi or a Linux based NAS. If you want to help me to get it to work under Windows, please contribute. ## Run -Clone the repo, setup config file (see below) and run `interface.py`. +Clone the repo, setup config file (see below) and run `interface.py`. Use your arrowkeys no navigate up and down the menu. +* **q** quit the interface +* **r** refresh the pending items by rescanning the filesystem. -## moviesort +## Movies Detect movie names by querying [themoviedb.org](https://www.themoviedb.org/) API and renaming the file based on a selection of possible matches. Follow the config file instructions bellow to get your API key. All data is courtesy of [The Movie Database](https://www.themoviedb.org), please contribute to this excellent database. @@ -14,23 +16,23 @@ All data is courtesy of [The Movie Database](https://www.themoviedb.org), please Movies will get renamed to this nameing style, a more flexible solution is in pending: **{movie-name} {Year}/{movie-name} {Year}.{ext}** -## tvsort +## TV shows Detect tv show filenames by querying the publicly available [tvmaze.com](https://www.tvmaze.com/) API to identify the show name and the episode name based on a selection of possible matches. Episodes are named in this style, a more flexible solution is in pending: **{show-name}/Season {nr}/show-name - S{nr}E{nr} - {episode-name}.{ext}** -## db_export -Export the library to csv files. Calles the Emby API to get a list of movies and episodes and exports this to a convenient set ov CSV files. - -## trailers +## Trailer download Downloading trailers from links provided from emby and move them into the movie folder. Trailers are named in this style, a more flexible solution is in pending: **{movie-name} {Year}_{youtube-id}_trailer.mkv** -## bad_id +## Fix Movie Names Sometimes Emby get's it wrong. Sometimes this script can get it wrong too. The *Fix Movie Names* function goes through the movie library looking for filenames that don't match with the movie name as identified in emby. +## DB export +Export the library to csv files. Calles the Emby API to get a list of movies and episodes and exports this to a convenient set ov CSV files. + ## setup ### install These are the none standard Python libraries in use in this project: @@ -61,9 +63,14 @@ Duplicate the config.sample.json file to a file named *config.json* and set the * `min_file_size`: Minimal filesize to be considered a relevant media file in bytes. #### Emby integration +*optional:* remove the 'emby' key from config.json to disable the emby integration. * `emby_url`: url where your emby instance is reachable * `emby_user_id`: user id of your emby user * `emby_api_key`: api key for your user on emby #### ydl_opts *Trailer download:* -Arguments under the [ydl_opts] section will get passed in to youtube-dl for *trailers*. Check out the documentation for details. \ No newline at end of file +*optional:* remove the 'ydl_opts' key from config.json to disable the trailer download functionality. +Arguments under the [ydl_opts] section will get passed in to youtube-dl for *trailers*. Check out the documentation for details. + +## Known limitations: +Most likely *media_organizer* will fail if there are any files like Outtakes, Extras, Feauturettes, etc in the folder. Should there be any files like that in the folder, moove/delete them first before opening *media_organizer*. \ No newline at end of file diff --git a/interface.py b/interface.py index 3f5ecfe..a8d0ca7 100755 --- a/interface.py +++ b/interface.py @@ -138,15 +138,19 @@ class Interface(): def print_menu(self, current_row_idx): """ print menu with populated pending count """ # build stdscr - menu = self.menu - h, w = self.stdscr.getmaxyx() - longest = len(max(menu)) - x = w // 2 - longest + max_h, max_w = self.stdscr.getmaxyx() + longest = len(max(self.menu)) + x = max_w // 2 - longest // 2 - 2 + first_menu = max_h // 2 - len(self.menu) // 2 self.stdscr.clear() - # help - self.stdscr.addstr(h - 1, x, 'q: quit, r: refresh') + # menu strings + url = 'github.com/bbilly1/media_organizer' + help_str = 'q: quit, r: refresh' + self.stdscr.addstr(max_h - 2, max_w // 2 - len(help_str) // 2, help_str) + self.stdscr.addstr(max_h - 1, max_w // 2 - len(url) // 2, url) + self.stdscr.addstr(first_menu - 2, x, 'Media Organizer') # loop through menu items - for idx, row in enumerate(menu): + for idx, row in enumerate(self.menu): # menu items count if row == 'All': pending_count = self.pending['total'] @@ -161,7 +165,7 @@ class Interface(): else: pending_count = ' ' # center whole - y = h // 2 - len(menu) + idx + y = first_menu + idx # print string to menu text = f'[{pending_count}] {row}' if idx == current_row_idx: From 634c4d6631350ea43a390511674842a4713acd8e Mon Sep 17 00:00:00 2001 From: simon Date: Sun, 30 May 2021 12:26:59 +0700 Subject: [PATCH 7/7] handle optional config keys in sel_handler All --- interface.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/interface.py b/interface.py index a8d0ca7..40e4fe8 100755 --- a/interface.py +++ b/interface.py @@ -115,15 +115,16 @@ class Interface(): # clean exit on ctrl + c return 'Exit' - @staticmethod - def sel_handler(menu_item): + def sel_handler(self, menu_item): """ lunch scripts from here based on selection """ if menu_item == 'All': moviesort.main() tvsort.main() - trailers.main() - id_fix.main() - db_export.main() + if 'ydl_opts' in self.CONFIG.keys(): + trailers.main() + if 'emby' in self.CONFIG.keys(): + id_fix.main() + db_export.main() elif menu_item == 'Movies': moviesort.main() elif menu_item == 'TV shows': @@ -145,8 +146,8 @@ class Interface(): self.stdscr.clear() # menu strings url = 'github.com/bbilly1/media_organizer' - help_str = 'q: quit, r: refresh' - self.stdscr.addstr(max_h - 2, max_w // 2 - len(help_str) // 2, help_str) + h_str = 'q: quit, r: refresh' + self.stdscr.addstr(max_h - 2, max_w // 2 - len(h_str) // 2, h_str) self.stdscr.addstr(max_h - 1, max_w // 2 - len(url) // 2, url) self.stdscr.addstr(first_menu - 2, x, 'Media Organizer') # loop through menu items