merging new interface class
This commit is contained in:
commit
9e3d42e313
27
README.md
27
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.
|
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*.
|
271
interface.py
271
interface.py
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue