merging new movie end episode classes
This commit is contained in:
commit
07e135ecdb
|
@ -62,7 +62,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 API is reachable
|
||||
|
|
|
@ -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",
|
||||
|
|
35
interface.py
35
interface.py
|
@ -2,43 +2,24 @@
|
|||
""" curses interface to lunch moviesort and tvsort """
|
||||
|
||||
import curses
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from os import path
|
||||
from time import sleep
|
||||
|
||||
from src.config import get_config
|
||||
|
||||
import src.tvsort as tvsort
|
||||
import src.tvsort_id as tvsort_id
|
||||
import src.moviesort as moviesort
|
||||
import src.db_export as db_export
|
||||
import src.trailers as trailers
|
||||
import src.id_fix as id_fix
|
||||
|
||||
|
||||
def get_config():
|
||||
""" read out config file and return config dict """
|
||||
# build path
|
||||
root_folder = path.dirname(sys.argv[0])
|
||||
if root_folder == '/sbin':
|
||||
# running interactive
|
||||
config_path = 'config.json'
|
||||
else:
|
||||
config_path = path.join(root_folder, 'config.json')
|
||||
# parse
|
||||
with open(config_path, 'r') as config_file:
|
||||
data = config_file.read()
|
||||
config = json.loads(data)
|
||||
return 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_tv = tvsort.get_pending(tv_downpath)
|
||||
pending_movie = moviesort.MovieHandler().pending
|
||||
pending_tv = tvsort.TvHandler().pending
|
||||
pending_trailer = len(trailers.get_pending(config))
|
||||
pending_movie_fix = len(id_fix.get_pending(config))
|
||||
pending_total = pending_movie + pending_tv + pending_trailer + pending_movie_fix
|
||||
|
@ -91,15 +72,15 @@ 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)
|
||||
tvsort.main(config, tvsort_id)
|
||||
moviesort.main()
|
||||
tvsort.main()
|
||||
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)
|
||||
tvsort.main()
|
||||
elif menu_item == 'DB export':
|
||||
db_export.main(config)
|
||||
elif menu_item == 'Trailer download':
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
""" config as separate module to avoid circular import """
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from os import path
|
||||
|
||||
|
||||
def get_config():
|
||||
""" read out config file and return config dict """
|
||||
# build path
|
||||
root_folder = path.dirname(sys.argv[0])
|
||||
if root_folder == '/sbin':
|
||||
# running interactive
|
||||
config_path = 'config.json'
|
||||
else:
|
||||
config_path = path.join(root_folder, 'config.json')
|
||||
# parse
|
||||
with open(config_path, 'r') as config_file:
|
||||
data = config_file.read()
|
||||
config = json.loads(data)
|
||||
return config
|
461
src/moviesort.py
461
src/moviesort.py
|
@ -3,261 +3,260 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
import requests
|
||||
|
||||
from src.config 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:
|
||||
new_filename = movie.movie_details['new_filename']
|
||||
old_file = os.path.join(sortpath, movie.filename)
|
||||
new_file = os.path.join(sortpath, 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, 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:
|
||||
new_filename = movie.movie_details['new_filename']
|
||||
print(f'from: {movie.filename} \nto: {new_filename}\n')
|
||||
to_continue = input('\ncontinue? Y/n')
|
||||
if to_continue == 'n':
|
||||
print('cancle...')
|
||||
return False
|
||||
moved = []
|
||||
for movie in identified:
|
||||
new_filename = movie.movie_details['new_filename']
|
||||
year_dedected = movie.movie_details['year_dedected']
|
||||
new_moviename = movie.movie_details['new_moviename']
|
||||
new_filename = movie.movie_details['new_filename']
|
||||
old_file = os.path.join(sortpath, new_filename)
|
||||
new_folder = os.path.join(
|
||||
moviepath, str(year_dedected), new_moviename
|
||||
)
|
||||
new_file = os.path.join(new_folder, new_filename)
|
||||
try:
|
||||
os.makedirs(new_folder)
|
||||
except FileExistsError:
|
||||
print(f'{movie_name}\nalready exists in archive')
|
||||
print(f'{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(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.file_parsed = self.split_filename()
|
||||
self.movie_details = 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('.')
|
||||
moviename_encoded = self.encode_moviename(moviename)
|
||||
# build file_parsed dict
|
||||
file_parsed = {}
|
||||
file_parsed['moviename'] = moviename
|
||||
file_parsed['moviename_encoded'] = moviename_encoded
|
||||
file_parsed['year'] = int(year)
|
||||
file_parsed['file_ext'] = file_ext
|
||||
return file_parsed
|
||||
|
||||
@ staticmethod
|
||||
def encode_moviename(moviename):
|
||||
""" url encode and clean the moviename """
|
||||
encoded = 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.file_parsed['year']
|
||||
moviename_encoded = self.file_parsed['moviename_encoded']
|
||||
# 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={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 """
|
||||
file_ext = self.file_parsed['file_ext']
|
||||
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}{file_ext}'
|
||||
movie_details = {}
|
||||
movie_details['new_moviename'] = new_moviename
|
||||
movie_details['new_filename'] = new_filename
|
||||
movie_details['year_dedected'] = year_dedected
|
||||
return movie_details
|
||||
|
||||
|
||||
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:
|
||||
if not handler.pending:
|
||||
print('no movies to sort')
|
||||
sleep(2)
|
||||
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)
|
||||
print(f'renamed {moved} movies')
|
||||
handler.cleanup(moved)
|
||||
|
|
455
src/tvsort.py
455
src/tvsort.py
|
@ -1,81 +1,410 @@
|
|||
""" handles moving tv downloads """
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
import requests
|
||||
|
||||
def get_pending(tv_downpath):
|
||||
""" return how many shows are pending """
|
||||
pending = len(os.listdir(tv_downpath))
|
||||
return pending
|
||||
from src.config import get_config
|
||||
|
||||
|
||||
def move_to_sort(tv_downpath, sortpath, ext):
|
||||
""" move tv files to sortpath """
|
||||
for dirpath, _, filenames in os.walk(tv_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 > 50000000:
|
||||
move_to = os.path.join(sortpath, filename)
|
||||
os.rename(path, move_to)
|
||||
if os.listdir(sortpath):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
class Static():
|
||||
""" staticmethods collection used from EpisodeIdentify """
|
||||
|
||||
@staticmethod
|
||||
def split_file_name(filename):
|
||||
"""
|
||||
takes the file name, returns showname, season, episode and id_style
|
||||
based on regex match
|
||||
"""
|
||||
multi_reg = r'[sS][0-9]{1,3}[eE][0-9]{1,3}-?[eE][0-9]{1,3}'
|
||||
if re.compile(multi_reg).findall(filename):
|
||||
# S01E01-02
|
||||
season_id_pattern = re.compile(multi_reg)
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
season = str(get_s_nr.findall(season_id)[0])
|
||||
e_list = get_s_nr.findall(season_id)[1:]
|
||||
episode = ' '.join(e_list)
|
||||
id_style = 'multi'
|
||||
elif re.compile(r'[sS][0-9]{1,3} ?[eE][0-9]{1,3}').findall(filename):
|
||||
# S01E01
|
||||
season_id_pattern = re.compile(r'[sS]\d{1,3} ?[eE]\d{1,3}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
season = str(get_s_nr.findall(season_id)[0])
|
||||
episode = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
elif re.compile(r'[0-9]{4}.[0-9]{2}.[0-9]{2}').findall(filename):
|
||||
# YYYY.MM.DD
|
||||
season_id_pattern = re.compile(r'[0-9]{4}.[0-9]{2}.[0-9]{2}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
season = "NA"
|
||||
episode = "NA"
|
||||
id_style = 'year'
|
||||
elif re.compile(r'0?[0-9][xX][0-9]{1,2}').findall(filename):
|
||||
# 01X01
|
||||
season_id_pattern = re.compile(r'0?[0-9][xX][0-9]{2}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
season = str(get_s_nr.findall(season_id)[0])
|
||||
episode = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
elif re.compile(r'[sS][0-9]{1,3}.?[eE][0-9]{1,3}').findall(filename):
|
||||
# S01*E01
|
||||
season_id_pattern = re.compile(r'[sS]\d{1,3}.?[eE]\d{1,3}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
season = str(get_s_nr.findall(season_id)[0])
|
||||
episode = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
else:
|
||||
# id syle not dealt with
|
||||
print('season episode id failed for:')
|
||||
print(filename)
|
||||
raise ValueError
|
||||
return season, episode, season_id, id_style
|
||||
|
||||
@staticmethod
|
||||
def showname_encoder(showname):
|
||||
""" encodes showname for best possible match """
|
||||
# tvmaze doesn't like years in showname
|
||||
showname = showname.strip().rstrip('.')
|
||||
year_pattern = re.compile(r'\(?[0-9]{4}\)?')
|
||||
year = year_pattern.findall(showname)
|
||||
if year:
|
||||
showname = showname.rstrip(str(year))
|
||||
encoded = showname.replace(" ", "%20")
|
||||
encoded = encoded.replace(".", "%20").replace("'", "%20")
|
||||
return encoded
|
||||
|
||||
@staticmethod
|
||||
def tvmaze_request(url):
|
||||
""" call the api with back_off on rate limit and user-agent """
|
||||
headers = {
|
||||
'User-Agent': 'https://github.com/bbilly1/media_organizer'
|
||||
}
|
||||
# retry up to 5 times
|
||||
for i in range(5):
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.ok:
|
||||
# all good
|
||||
break
|
||||
if response.status_code == 429:
|
||||
# rate limited
|
||||
print('hit tvmaze rate limiting, slowing down')
|
||||
else:
|
||||
# general fail
|
||||
print('request failed with url:\n' + url)
|
||||
# slow down
|
||||
back_off = (i + 1) ** 2
|
||||
sleep(back_off)
|
||||
request = response.json()
|
||||
return request
|
||||
|
||||
|
||||
def move_to_archive(sortpath, tvpath):
|
||||
""" moves the renamed files to the archive """
|
||||
print()
|
||||
for dirpath, _, filenames in os.walk(sortpath):
|
||||
for show in sorted(filenames):
|
||||
print(show)
|
||||
input('\ncontinue?')
|
||||
# apply
|
||||
for dirpath, _, filenames in os.walk(sortpath):
|
||||
for show in filenames:
|
||||
old_file = os.path.join(sortpath, dirpath, show)
|
||||
show_name = dirpath.split('/')[-2]
|
||||
season_name = dirpath.split('/')[-1]
|
||||
new_folder = os.path.join(tvpath, show_name, season_name)
|
||||
new_file = os.path.join(new_folder, show)
|
||||
class Episode():
|
||||
""" describes single episode """
|
||||
|
||||
def __init__(self, filename, discovered):
|
||||
self.filename = filename
|
||||
self.discovered = discovered
|
||||
self.file_parsed = self.parse_filename()
|
||||
|
||||
showname = self.file_parsed['showname']
|
||||
show_id = None
|
||||
showname_clean = None
|
||||
for i in discovered:
|
||||
if showname == i['showname']:
|
||||
# found it
|
||||
show_id = i['show_id']
|
||||
showname_clean = i['showname_clean']
|
||||
break
|
||||
if not show_id and not showname_clean:
|
||||
self.all_results = self.get_show_id()
|
||||
|
||||
self.episode_details = self.get_ep_details(show_id, showname_clean)
|
||||
|
||||
def parse_filename(self):
|
||||
""" parse the file name into its parts """
|
||||
filename = self.filename
|
||||
season, episode, season_id, id_style = Static.split_file_name(filename)
|
||||
showname = filename.split(season_id)[0]
|
||||
ext = os.path.splitext(filename)[1]
|
||||
encoded = Static.showname_encoder(showname)
|
||||
# build file_parsed dict
|
||||
file_parsed = {}
|
||||
file_parsed['showname'] = encoded
|
||||
file_parsed['season'] = season
|
||||
file_parsed['episode'] = episode
|
||||
file_parsed['season_id'] = season_id
|
||||
file_parsed['id_style'] = id_style
|
||||
file_parsed['ext'] = ext
|
||||
# return dict
|
||||
return file_parsed
|
||||
|
||||
def get_show_id(self):
|
||||
""" return dict of matches """
|
||||
showname = self.file_parsed['showname']
|
||||
url = 'http://api.tvmaze.com/search/shows?q=' + showname
|
||||
request = Static.tvmaze_request(url)
|
||||
# loop through results
|
||||
all_results = []
|
||||
for idx, result in enumerate(request):
|
||||
list_id = idx
|
||||
show_id = result['show']['id']
|
||||
showname_clean = result['show']['name']
|
||||
status = result['show']['status']
|
||||
desc_raw = result['show']['summary']
|
||||
# filter out basic html tags
|
||||
try:
|
||||
desc = re.sub('<[^<]+?>', '', desc_raw)
|
||||
except TypeError:
|
||||
desc = desc_raw
|
||||
result_dict = {}
|
||||
result_dict['list_id'] = list_id
|
||||
result_dict['show_id'] = show_id
|
||||
result_dict['showname_clean'] = showname_clean
|
||||
result_dict['desc'] = desc
|
||||
result_dict['status'] = status
|
||||
all_results.append(result_dict)
|
||||
# return all_results dict
|
||||
return all_results
|
||||
|
||||
def pick_show_id(self):
|
||||
""" simple menu to pick matching show manually """
|
||||
all_results = self.all_results
|
||||
filename = self.filename
|
||||
# more than one possibility
|
||||
if len(all_results) > 1:
|
||||
print(f'\nfilename: {filename}')
|
||||
# print menu
|
||||
for i in all_results:
|
||||
list_id = i['list_id']
|
||||
showname_clean = i['showname_clean']
|
||||
message = f'[{list_id}] {showname_clean}'
|
||||
print(message)
|
||||
print('[?] show more\n')
|
||||
# select
|
||||
select = input('select: ')
|
||||
# long menu with desc
|
||||
if select == '?':
|
||||
# print menu
|
||||
for i in all_results[:5]:
|
||||
list_id = i['list_id']
|
||||
showname_clean = i['showname_clean']
|
||||
status = i['status']
|
||||
desc = i['desc']
|
||||
message = (f'[{list_id}] {showname_clean},'
|
||||
+ f'status: {status}\n{desc}\n')
|
||||
print(message)
|
||||
# select
|
||||
select = input('select: ')
|
||||
else:
|
||||
# only one possibility
|
||||
select = 0
|
||||
# build string based on selected
|
||||
index = int(select)
|
||||
show_id = all_results[index]['show_id']
|
||||
showname_clean = all_results[index]['showname_clean']
|
||||
# return tuble
|
||||
return show_id, showname_clean
|
||||
|
||||
def get_ep_details(self, show_id=None, showname_clean=None):
|
||||
""" build the show details dict"""
|
||||
if not show_id and not showname_clean:
|
||||
show_id, showname_clean = self.pick_show_id()
|
||||
season, episode, episode_name = self.get_episode_name(show_id)
|
||||
episode_details = {}
|
||||
episode_details['show_id'] = show_id
|
||||
episode_details['showname_clean'] = showname_clean
|
||||
episode_details['season'] = season
|
||||
episode_details['episode'] = episode
|
||||
episode_details['episode_name'] = episode_name
|
||||
return episode_details
|
||||
|
||||
def multi_parser(self, show_id):
|
||||
""" parse multi episode files names for get_episode_name() """
|
||||
file_parsed = self.filename
|
||||
season = file_parsed['season']
|
||||
episode_list = file_parsed['episode'].split()
|
||||
# loop through all episodes
|
||||
episode_name_list = []
|
||||
for episode in episode_list:
|
||||
url = (f'http://api.tvmaze.com/shows/{show_id}/episodebynumber?'
|
||||
f'season={season}&number={episode}')
|
||||
request = Static.tvmaze_request(url)
|
||||
episode_name = request['name']
|
||||
episode_name_list.append(episode_name)
|
||||
|
||||
episode = '-E'.join(episode_list)
|
||||
episode_name = ', '.join(episode_name_list)
|
||||
return season, episode, episode_name
|
||||
|
||||
def get_episode_name(self, show_id):
|
||||
""" find episode based on show_id and id_style """
|
||||
file_parsed = self.file_parsed
|
||||
id_style = file_parsed['id_style']
|
||||
# multi episode filename
|
||||
if id_style == 'multi':
|
||||
# build and return tuple on multi episode
|
||||
season, episode, episode_name = self.multi_parser(show_id)
|
||||
return season, episode, episode_name
|
||||
# season - episode based
|
||||
if id_style == 'se':
|
||||
season = file_parsed['season']
|
||||
episode = file_parsed['episode']
|
||||
url = (f'http://api.tvmaze.com/shows/{show_id}/episodebynumber?'
|
||||
f'season={season}&number={episode}')
|
||||
request = Static.tvmaze_request(url)
|
||||
# returns a dict
|
||||
show_response = request
|
||||
# date based
|
||||
elif id_style == 'year':
|
||||
date_raw = file_parsed['season_id']
|
||||
year, month, day = date_raw.split('.')
|
||||
url = (f'https://api.tvmaze.com/shows/{show_id}/episodesbydate?'
|
||||
f'date={year}-{month}-{day}')
|
||||
request = Static.tvmaze_request(url)
|
||||
# returns a list
|
||||
show_response = request[0]
|
||||
# build and return tuple
|
||||
season = str(show_response['season']).zfill(2)
|
||||
episode = str(show_response['number']).zfill(2)
|
||||
episode_name = show_response['name']
|
||||
return season, episode, episode_name
|
||||
|
||||
|
||||
class TvHandler():
|
||||
""" handles the tv sort classes """
|
||||
|
||||
CONFIG = get_config()
|
||||
|
||||
def __init__(self):
|
||||
self.pending = self.get_pending()
|
||||
self.discovered = []
|
||||
|
||||
def get_pending(self):
|
||||
""" return how many shows are pending """
|
||||
tv_downpath = self.CONFIG['media']['tv_downpath']
|
||||
pending = len(os.listdir(tv_downpath))
|
||||
return pending
|
||||
|
||||
def move_to_sort(self):
|
||||
""" move tv files to sortpath """
|
||||
tv_downpath = self.CONFIG['media']['tv_downpath']
|
||||
ext = self.CONFIG['media']['ext']
|
||||
min_file_size = self.CONFIG['media']['min_file_size']
|
||||
sortpath = self.CONFIG['media']['sortpath']
|
||||
# walk through tv_downpath
|
||||
for dirpath, _, filenames in os.walk(tv_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 episode_identify(self, to_rename):
|
||||
""" loops through the pending list """
|
||||
identified = []
|
||||
for filename in to_rename:
|
||||
episode = Episode(filename, self.discovered)
|
||||
# add to discovered
|
||||
showname = episode.file_parsed['showname']
|
||||
showname_clean = episode.episode_details['showname_clean']
|
||||
show_id = episode.episode_details['show_id']
|
||||
discovered_item = {}
|
||||
discovered_item['showname'] = showname
|
||||
discovered_item['showname_clean'] = showname_clean
|
||||
discovered_item['show_id'] = show_id
|
||||
self.discovered.append(discovered_item)
|
||||
identified.append(episode)
|
||||
return identified
|
||||
|
||||
def episode_rename(self, identified):
|
||||
""" make folder and rename files as identified """
|
||||
sortpath = self.CONFIG['media']['sortpath']
|
||||
renamed = []
|
||||
for episode in identified:
|
||||
# build vars
|
||||
ext = episode.file_parsed['ext']
|
||||
showname_clean = episode.episode_details['showname_clean']
|
||||
season = episode.episode_details['season']
|
||||
season_int = int(season)
|
||||
episode_id = episode.episode_details['episode']
|
||||
episode_name = episode.episode_details['episode_name']
|
||||
# build paths
|
||||
old_file = os.path.join(sortpath, episode.filename)
|
||||
new_folder = os.path.join(sortpath, showname_clean,
|
||||
f'Season {season_int}')
|
||||
new_file_name = (f'{showname_clean} - S{season}E{episode_id} - '
|
||||
+ f'{episode_name}{ext}')
|
||||
new_file = os.path.join(new_folder, new_file_name)
|
||||
# do it
|
||||
os.makedirs(new_folder, exist_ok=True)
|
||||
os.rename(old_file, new_file)
|
||||
# finish up
|
||||
print(episode.filename)
|
||||
renamed.append(new_file)
|
||||
logging.info('tv:from [%s] to [%s]', episode.filename, new_file)
|
||||
return renamed
|
||||
|
||||
def move_to_archive(self):
|
||||
""" moves the renamed files to the archive """
|
||||
sortpath = self.CONFIG['media']['sortpath']
|
||||
tvpath = self.CONFIG['media']['tvpath']
|
||||
print()
|
||||
for dirpath, _, filenames in os.walk(sortpath):
|
||||
for show in sorted(filenames):
|
||||
print(show)
|
||||
input('\ncontinue?')
|
||||
# apply
|
||||
for dirpath, _, filenames in os.walk(sortpath):
|
||||
for show in filenames:
|
||||
# make folders
|
||||
folder_name = dirpath.lstrip(sortpath)
|
||||
new_folder = os.path.join(tvpath, folder_name)
|
||||
os.makedirs(new_folder, exist_ok=True)
|
||||
# move file
|
||||
old_file = os.path.join(sortpath, dirpath, show)
|
||||
new_file = os.path.join(new_folder, show)
|
||||
os.rename(old_file, new_file)
|
||||
|
||||
def clean_up(self):
|
||||
""" clean up download and sort folder """
|
||||
sortpath = self.CONFIG['media']['sortpath']
|
||||
tv_downpath = self.CONFIG['media']['tv_downpath']
|
||||
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])
|
||||
to_clean_list = os.listdir(tv_downpath)
|
||||
for to_clean in to_clean_list:
|
||||
to_trash = os.path.join(tv_downpath, to_clean)
|
||||
subprocess.call(["trash", to_trash])
|
||||
|
||||
|
||||
def clean_up(sortpath, tv_downpath):
|
||||
""" clean up download and sort folder """
|
||||
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])
|
||||
to_clean_list = os.listdir(tv_downpath)
|
||||
for to_clean in to_clean_list:
|
||||
to_trash = os.path.join(tv_downpath, to_clean)
|
||||
subprocess.call(["trash", to_trash])
|
||||
|
||||
|
||||
def main(config, tvsort_id):
|
||||
def main():
|
||||
""" main function to sort tv shows """
|
||||
# parse config
|
||||
tv_downpath = config['media']['tv_downpath']
|
||||
tvpath = config['media']['tvpath']
|
||||
sortpath = config['media']['sortpath']
|
||||
ext = config['media']['ext']
|
||||
# stop here if nothing to do
|
||||
pending = get_pending(tv_downpath)
|
||||
if not pending:
|
||||
print('no tv shows to sort')
|
||||
sleep(2)
|
||||
handler = TvHandler()
|
||||
if not handler.pending:
|
||||
print('no tvshows to sort')
|
||||
return
|
||||
# move files
|
||||
to_sort = move_to_sort(tv_downpath, sortpath, ext)
|
||||
if to_sort:
|
||||
renamed = tvsort_id.episode_rename(config)
|
||||
to_rename = handler.move_to_sort()
|
||||
if to_rename:
|
||||
identified = handler.episode_identify(to_rename)
|
||||
renamed = handler.episode_rename(identified)
|
||||
if renamed:
|
||||
move_to_archive(sortpath, tvpath)
|
||||
clean_up(sortpath, tv_downpath)
|
||||
handler.move_to_archive()
|
||||
print(f'renamed {len(renamed)} movies')
|
||||
handler.clean_up()
|
||||
|
|
270
src/tvsort_id.py
270
src/tvsort_id.py
|
@ -1,270 +0,0 @@
|
|||
""" handles id and renaming tv download files """
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from time import sleep
|
||||
|
||||
|
||||
def split_file_name(filename):
|
||||
"""
|
||||
takes the file name, returns showname, season, episode and id_style
|
||||
based on regex match
|
||||
"""
|
||||
if re.compile(r'[sS][0-9]{1,3}[eE][0-9]{1,3}-?[eE][0-9]{1,3}').findall(filename):
|
||||
# S01E01-02
|
||||
season_id_pattern = re.compile(r'[sS][0-9]{1,3}[eE][0-9]{1,3}-?[eE][0-9]{1,3}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
s = str(get_s_nr.findall(season_id)[0])
|
||||
e_list = get_s_nr.findall(season_id)[1:]
|
||||
e = ' '.join(e_list)
|
||||
id_style = 'multi'
|
||||
elif re.compile(r'[sS][0-9]{1,3} ?[eE][0-9]{1,3}').findall(filename):
|
||||
# S01E01
|
||||
season_id_pattern = re.compile(r'[sS]\d{1,3} ?[eE]\d{1,3}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
s = str(get_s_nr.findall(season_id)[0])
|
||||
e = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
elif re.compile(r'[0-9]{4}.[0-9]{2}.[0-9]{2}').findall(filename):
|
||||
# YYYY.MM.DD
|
||||
season_id_pattern = re.compile(r'[0-9]{4}.[0-9]{2}.[0-9]{2}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
s = "NA"
|
||||
e = "NA"
|
||||
id_style = 'year'
|
||||
elif re.compile(r'0?[0-9][xX][0-9]{1,2}').findall(filename):
|
||||
# 01X01
|
||||
season_id_pattern = re.compile(r'0?[0-9][xX][0-9]{2}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
s = str(get_s_nr.findall(season_id)[0])
|
||||
e = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
elif re.compile(r'[sS][0-9]{1,3}.?[eE][0-9]{1,3}').findall(filename):
|
||||
# S01*E01
|
||||
season_id_pattern = re.compile(r'[sS]\d{1,3}.?[eE]\d{1,3}')
|
||||
season_id = season_id_pattern.findall(filename)[0]
|
||||
get_s_nr = re.compile(r'[0-9]{1,3}')
|
||||
s = str(get_s_nr.findall(season_id)[0])
|
||||
e = str(get_s_nr.findall(season_id)[1])
|
||||
id_style = 'se'
|
||||
else:
|
||||
# id syle not dealt with
|
||||
print('season episode id failed for:')
|
||||
print(filename)
|
||||
raise ValueError
|
||||
showname = filename.split(season_id)[0]
|
||||
encoded = showname_encoder(showname)
|
||||
# build file_details dict
|
||||
file_details = {}
|
||||
file_details['showname'] = encoded
|
||||
file_details['season'] = s
|
||||
file_details['episode'] = e
|
||||
file_details['season_id'] = season_id
|
||||
file_details['id_style'] = id_style
|
||||
# return dict
|
||||
return file_details
|
||||
|
||||
|
||||
def showname_encoder(showname):
|
||||
""" encodes showname for best possible match """
|
||||
# tvmaze doesn't like years in showname
|
||||
showname = showname.strip().rstrip('.').rstrip('-').strip()
|
||||
year_pattern = re.compile(r'\(?[0-9]{4}\)?')
|
||||
year = year_pattern.findall(showname)
|
||||
if year:
|
||||
showname = showname.rstrip(str(year))
|
||||
encoded = showname.replace(" ", "%20")\
|
||||
.replace(".", "%20").replace("'", "%20")
|
||||
return encoded
|
||||
|
||||
|
||||
def tvmaze_request(url):
|
||||
""" call the api with back_off on rate limit and user-agent """
|
||||
headers = {
|
||||
'User-Agent': 'https://github.com/bbilly1/media_organizer'
|
||||
}
|
||||
# retry up to 5 times
|
||||
for i in range(5):
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.ok:
|
||||
# all good
|
||||
break
|
||||
elif response.status_code == 429:
|
||||
# rate limited
|
||||
print('hit tvmaze rate limiting, slowing down')
|
||||
back_off = (i + 1) ** 2
|
||||
sleep(back_off)
|
||||
else:
|
||||
# all failed
|
||||
print('request failed with url:\n' + url)
|
||||
request = response.json()
|
||||
return request
|
||||
|
||||
|
||||
def get_show_id(file_details):
|
||||
""" return dict of matches """
|
||||
showname = file_details['showname']
|
||||
url = 'http://api.tvmaze.com/search/shows?q=' + showname
|
||||
request = tvmaze_request(url)
|
||||
# loop through results
|
||||
all_results = []
|
||||
for idx, result in enumerate(request):
|
||||
list_id = idx
|
||||
show_id = result['show']['id']
|
||||
showname_clean = result['show']['name']
|
||||
status = result['show']['status']
|
||||
desc_raw = result['show']['summary']
|
||||
# TODO better html extract method
|
||||
try:
|
||||
desc = re.sub('<[^<]+?>', '', desc_raw)
|
||||
except:
|
||||
desc = desc_raw
|
||||
result_dict = {}
|
||||
result_dict['list_id'] = list_id
|
||||
result_dict['show_id'] = show_id
|
||||
result_dict['showname_clean'] = showname_clean
|
||||
result_dict['desc'] = desc
|
||||
result_dict['status'] = status
|
||||
all_results.append(result_dict)
|
||||
# return all_results dict
|
||||
return all_results
|
||||
|
||||
|
||||
def pick_show_id(all_results, filename):
|
||||
""" simple menu to pick matching show manually """
|
||||
# more than one possibility
|
||||
if len(all_results) > 1:
|
||||
print(f'\nfilename: {filename}')
|
||||
# print menu
|
||||
for i in all_results:
|
||||
list_id = i['list_id']
|
||||
showname_clean = i['showname_clean']
|
||||
message = f'[{list_id}] {showname_clean}'
|
||||
print(message)
|
||||
print('[?] show more\n')
|
||||
# select
|
||||
select = input('select: ')
|
||||
# long menu with desc
|
||||
if select == '?':
|
||||
# print menu
|
||||
for i in all_results[:5]:
|
||||
list_id = i['list_id']
|
||||
showname_clean = i['showname_clean']
|
||||
status = i['status']
|
||||
desc = i['desc']
|
||||
message = f'[{list_id}] {showname_clean}, status: {status}\n{desc}\n'
|
||||
print(message)
|
||||
# select
|
||||
select = input('select: ')
|
||||
else:
|
||||
# only one possibility
|
||||
select = 0
|
||||
# build string based on selected
|
||||
index = int(select)
|
||||
show_id = all_results[index]['show_id']
|
||||
show_name_clean = all_results[index]['showname_clean']
|
||||
# return tuble
|
||||
return show_id, show_name_clean
|
||||
|
||||
|
||||
def multi_parser(file_details, show_id):
|
||||
""" parse multi episode files names """
|
||||
season = file_details['season']
|
||||
episode_list = file_details['episode'].split()
|
||||
# loop through all episodes
|
||||
episode_name_list = []
|
||||
for episode in episode_list:
|
||||
url = f'http://api.tvmaze.com/shows/{show_id}/episodebynumber?season={season}&number={episode}'
|
||||
request = tvmaze_request(url)
|
||||
episode_name = request['name']
|
||||
episode_name_list.append(episode_name)
|
||||
|
||||
episode = '-E'.join(episode_list)
|
||||
episode_name = ', '.join(episode_name_list)
|
||||
return season, episode, episode_name
|
||||
|
||||
|
||||
def get_episode_name(file_details, show_id):
|
||||
""" find episode based on show_id and id_style """
|
||||
id_style = file_details['id_style']
|
||||
# multi episode filename
|
||||
if id_style == 'multi':
|
||||
# build and return tuple on multi episode
|
||||
season, episode, episode_name = multi_parser(file_details, show_id)
|
||||
return season, episode, episode_name
|
||||
# season - episode based
|
||||
if id_style == 'se':
|
||||
season = file_details['season']
|
||||
episode = file_details['episode']
|
||||
url = f'http://api.tvmaze.com/shows/{show_id}/episodebynumber?season={season}&number={episode}'
|
||||
request = tvmaze_request(url)
|
||||
# returns a dict
|
||||
show_response = request
|
||||
# date based
|
||||
elif id_style == 'year':
|
||||
date_raw = file_details['season_id']
|
||||
year, month, day = date_raw.split('.')
|
||||
url = f'https://api.tvmaze.com/shows/{show_id}/episodesbydate?date={year}-{month}-{day}'
|
||||
request = tvmaze_request(url)
|
||||
# returns a list
|
||||
show_response = request[0]
|
||||
# build and return tuple
|
||||
season = str(show_response['season']).zfill(2)
|
||||
episode = str(show_response['number']).zfill(2)
|
||||
episode_name = show_response['name']
|
||||
return season, episode, episode_name
|
||||
|
||||
|
||||
def episode_rename(config):
|
||||
""" loops through all files in sortpath """
|
||||
sortpath = config['media']['sortpath']
|
||||
# poor man's cache
|
||||
cache = {}
|
||||
cache['last_show_name'] = None
|
||||
cache['last_show_id'] = None
|
||||
cache['last_show_name_clean'] = None
|
||||
# to rename
|
||||
to_rename = sorted(os.listdir(sortpath), key=str.casefold)
|
||||
# start the loop
|
||||
renamed = []
|
||||
for filename in to_rename:
|
||||
file_details = split_file_name(filename)
|
||||
last_show_name = file_details['showname'].lower()
|
||||
# check cach
|
||||
if last_show_name == cache['last_show_name']:
|
||||
# already in cache, no need to search again
|
||||
show_id = cache['last_show_id']
|
||||
show_name_clean = cache['last_show_name_clean']
|
||||
else:
|
||||
# not in cache, search
|
||||
all_results = get_show_id(file_details)
|
||||
show_id, show_name_clean = pick_show_id(all_results, filename)
|
||||
# update cache
|
||||
cache['last_show_name'] = last_show_name.lower()
|
||||
cache['last_show_id'] = show_id
|
||||
cache['last_show_name_clean'] = show_name_clean
|
||||
# get episode specific details
|
||||
season, episode, episode_name = get_episode_name(file_details, show_id)
|
||||
# invalid episode chars
|
||||
episode_name = episode_name.replace('/', '-')
|
||||
# build new filename
|
||||
ext = os.path.splitext(filename)[1]
|
||||
s_clean = season.lstrip('0')
|
||||
new_folder = os.path.join(sortpath, show_name_clean, 'Season ' + s_clean)
|
||||
rename_to = f'{show_name_clean} - S{season}E{episode} - {episode_name}{ext}'
|
||||
old_file = os.path.join(sortpath, filename)
|
||||
new_file = os.path.join(sortpath, new_folder, rename_to)
|
||||
# make folder and move
|
||||
os.makedirs(new_folder, exist_ok=True)
|
||||
os.rename(old_file, new_file)
|
||||
# output
|
||||
print(new_file)
|
||||
renamed.append(rename_to)
|
||||
logging.info('tv:from [{}] to [{}]'.format(filename,rename_to))
|
||||
# return new filenames
|
||||
return renamed
|
Loading…
Reference in New Issue