merging new interface class

This commit is contained in:
simon 2021-05-30 12:30:25 +07:00
commit 9e3d42e313
4 changed files with 188 additions and 127 deletions

View File

@ -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. 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 ## 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. 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. 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: Movies will get renamed to this nameing style, a more flexible solution is in pending:
**{movie-name} {Year}/{movie-name} {Year}.{ext}** **{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. 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: 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}** **{show-name}/Season {nr}/show-name - S{nr}E{nr} - {episode-name}.{ext}**
## db_export ## Trailer download
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
Downloading trailers from links provided from emby and move them into the movie folder. 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: Trailers are named in this style, a more flexible solution is in pending:
**{movie-name} {Year}_{youtube-id}_trailer.mkv** **{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. 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 ## setup
Needs Python >= 3.6 to run. 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. * `min_file_size`: Minimal filesize to be considered a relevant media file in bytes.
#### Emby integration #### 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_user_id`: user id of your emby user
* `emby_api_key`: api key for your user on emby * `emby_api_key`: api key for your user on emby
#### ydl_opts *Trailer download:* #### 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. *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*.

View File

@ -15,132 +15,175 @@ import src.trailers as trailers
import src.id_fix as id_fix import src.id_fix as id_fix
def get_pending_all(): class Interface():
""" figure out what needs to be done """ """ creating and removing the menu """
# 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
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): def __init__(self):
""" print menu with populated pending count """ self.menu = self.build_menu()
# build stdscr self.stdscr = None
h, w = stdscr.getmaxyx() self.menu_item = 0
longest = len(max(menu)) self.pending = self.get_pending_all()
x = w // 2 - longest
stdscr.clear() def get_pending_all(self):
# loop through menu items """ figure out what needs to be done """
for idx, row in enumerate(menu): # call subfunction to collect pending
# menu items count pending = {}
if row == 'All': pending_movie = moviesort.MovieHandler().pending
pending_count = pending['total'] pending_tv = tvsort.TvHandler().pending
elif row == 'Movies': pending['movies'] = pending_movie
pending_count = pending['movies'] pending['tv'] = pending_tv
elif row == 'TV shows': # based on config key
pending_count = pending['tv'] if 'emby' in self.CONFIG.keys():
elif row == 'Trailer download': pending_trailer = len(trailers.TrailerHandler().pending)
pending_count = pending['trailer'] pending_movie_fix = len(id_fix.MovieNameFix().pending)
elif row == 'Fix Movie Names': pending['trailer'] = pending_trailer
pending_count = pending['movie_fix'] pending['movie_fix'] = pending_movie_fix
pending_total = (pending_movie + pending_tv +
pending_trailer + pending_movie_fix)
else: else:
pending_count = ' ' pending_total = pending_movie + pending_tv
# center whole pending['total'] = pending_total
y = h // 2 - len(menu) + idx return pending
# 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()
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): def create_interface(self):
""" lunch scripts from here based on selection """ """ create the main loop for curses.wrapper """
if menu_item == 'All': while True:
moviesort.main() menu_item = curses.wrapper(self.curses_main)
tvsort.main() if menu_item != 'Exit':
db_export.main() self.sel_handler(menu_item)
trailers.main() sleep(3)
id_fix.main() self.pending = self.get_pending_all()
elif menu_item == 'Movies': else:
moviesort.main() return
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 curses_main(self, stdscr):
def curses_main(stdscr, menu): """ curses main to desplay and restart the menu """
""" curses main to desplay and restart the menu """ self.stdscr = stdscr
curses.curs_set(0) curses.curs_set(0)
curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_WHITE) curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_WHITE)
current_row_idx = 0 current_row_idx = 0
pending = get_pending_all() self.print_menu(current_row_idx)
print_menu(stdscr, current_row_idx, menu, pending) # endless loop
# endless loop while True:
while True: # wait for exit signal
# wait for exit signal try:
try: key = stdscr.getch()
key = stdscr.getch() stdscr.clear()
stdscr.clear() # react to kee press
# react to kee press last = len(self.menu) - 1
if key == curses.KEY_UP and current_row_idx > 0: if key == curses.KEY_UP and current_row_idx > 0:
current_row_idx -= 1 current_row_idx -= 1
elif key == curses.KEY_DOWN and current_row_idx < len(menu) - 1: elif key == curses.KEY_DOWN and current_row_idx < last:
current_row_idx += 1 current_row_idx += 1
elif key == curses.KEY_ENTER or key in [10, 13]: elif key == curses.KEY_ENTER or key in [10, 13]:
menu_item = menu[current_row_idx] menu_item = self.menu[current_row_idx]
stdscr.addstr(0, 0, f'start task: {menu_item}') 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() stdscr.refresh()
sleep(1) except KeyboardInterrupt:
# exit curses and do something # clean exit on ctrl + c
return menu_item return 'Exit'
# print
print_menu(stdscr, current_row_idx, menu, pending) def sel_handler(self, menu_item):
stdscr.refresh() """ lunch scripts from here based on selection """
except KeyboardInterrupt: if menu_item == 'All':
# clean exit on ctrl + c moviesort.main()
return 'Exit' 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(): def main():
""" main wraps the curses menu """ """ main wraps the curses menu """
# setup # setup
menu = ['All', 'Movies', 'TV shows', 'DB export', 'Trailer download', 'Fix Movie Names', 'Exit'] window = Interface()
config = get_config() window.create_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')
# 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)
# start here # start here

View File

@ -1,6 +1,7 @@
""" export collection from emby to CSV """ """ export collection from emby to CSV """
import csv import csv
from time import sleep
from os import path from os import path
import requests import requests
@ -27,8 +28,12 @@ class DatabaseExport():
'&fields=Genres,MediaStreams,Overview,' '&fields=Genres,MediaStreams,Overview,'
'ProviderIds,Path,RunTimeTicks' 'ProviderIds,Path,RunTimeTicks'
'&SortBy=DateCreated&SortOrder=Descending') '&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'] all_movies = response.json()['Items']
# episodes # episodes
url = (f'{emby_url}/Users/{emby_user_id}/Items?api_key={emby_api_key}' url = (f'{emby_url}/Users/{emby_user_id}/Items?api_key={emby_api_key}'
@ -36,8 +41,12 @@ class DatabaseExport():
'&Fields=DateCreated,Genres,MediaStreams,' '&Fields=DateCreated,Genres,MediaStreams,'
'MediaSources,Overview,ProviderIds,Path,RunTimeTicks' 'MediaSources,Overview,ProviderIds,Path,RunTimeTicks'
'&SortBy=DateCreated&SortOrder=Descending&IsMissing=false') '&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'] all_episodes = response.json()['Items']
return all_movies, all_episodes return all_movies, all_episodes

View File

@ -356,7 +356,9 @@ class TvHandler():
os.rename(old_file, new_file) os.rename(old_file, new_file)
# finish up # finish up
renamed.append(new_file) 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 return renamed
def move_to_archive(self): def move_to_archive(self):