diff --git a/README.md b/README.md index 8c4317d..8edb7f6 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 Needs Python >= 3.6 to run. @@ -66,9 +68,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 -* `emby_url`: url where your emby API is reachable +*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 f87c0d1..40e4fe8 100755 --- a/interface.py +++ b/interface.py @@ -15,132 +15,175 @@ 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 'emby' not in config_keys: + menu.remove('Fix Movie Names') + menu.remove('DB export') + if 'ydl_opts' not 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(menu_item) + sleep(3) + self.pending = self.get_pending_all() + 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, menu_item): + """ lunch scripts from here based on selection """ + if menu_item == 'All': + moviesort.main() + tvsort.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': + tvsort.main() + elif menu_item == 'Trailer download': + trailers.main() + elif menu_item == 'Fix Movie Names': + id_fix.main() + elif menu_item == 'DB export': + db_export.main() + + def print_menu(self, current_row_idx): + """ print menu with populated pending count """ + # build stdscr + 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() + # menu strings + url = 'github.com/bbilly1/media_organizer' + 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 + for idx, row in enumerate(self.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 = first_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 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 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):