* Rectified a bug where MediaLibrary would encounter an error due to a missing directory

* Enhanced clarity by refining comments
* Improved code readability through cleanup and organization
* Ensured consistency and enhanced readability by adjusting variable names
This commit is contained in:
Fabrice Quenneville 2024-04-24 01:18:24 -04:00
parent 44bf4fef04
commit 3ec56f4a66
4 changed files with 237 additions and 271 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
''' This is the library object who holds the information about the workspace and all the videos in it.''' ''' This module defines the MediaLibrary class, which manages information about the workspace and all videos in it.'''
from pathlib import Path from pathlib import Path
@ -9,184 +9,159 @@ from .tools import deletefile
import colorama import colorama
colorama.init() colorama.init()
class MediaLibrary(): cgreen = colorama.Fore.GREEN
''' This is the library object who holds the information about the workspace and all the videos in it. ''' creset = colorama.Fore.RESET
def __init__(self, files = False, directories = False, inputs = ["any"], filters = [], verbose = False): class MediaLibrary():
'''This class manages information about the workspace and all videos in it.'''
def __init__(self, files=False, directories=False, inputs=["any"], filters=[], verbose=False):
''' '''
This is the library object who holds the information about the workspace and all the videos in it. Initializes a MediaLibrary instance with provided parameters.
Args: Args:
files = False : A list of video files files (list or False): A list of video files.
directories = False : A list of directories containing videos directly or in the subdirectories directories (list or False): A list of directories containing videos directly or in subdirectories.
inputs = ["any"] : A list of filters to keep when browsing the directories inputs (list): A list of filters to keep when browsing the directories.
filters = [] : A list of filters to apply to the videos filters (list): A list of filters to apply to the videos.
verbose = False : A list of print options verbose (bool): A flag to enable verbose output.
Returns:
''' '''
if not files and not directories: if not files and not directories:
return return
self.directories = None self.directories = None
self.inputs = inputs self.inputs = inputs
self.filters = filters self.filters = filters
self.videos = dict() self.videos = dict()
if files: if files:
for filepath in files: for filepath in files:
self.videos[filepath] = Video(filepath, verbose = verbose) self.videos[filepath] = Video(filepath, verbose=verbose)
if directories: if directories:
self.directories = directories self.directories = directories
self.load_directories(verbose = verbose) self.load_directories(verbose=verbose)
self.filter_videos(verbose = verbose) self.filter_videos(verbose=verbose)
def __str__(self): def __str__(self):
''' Returns a sting representations about the current instance of the MediaLibrary object '''
Returns a string representation of the MediaLibrary instance.
Args:
Returns: Returns:
String : Information about what the MediaLibrary is tracking str: Information about the MediaLibrary instance.
''' '''
text = ""
if self.directories: if self.directories:
text = f"MediaCurator is watching the following directories: " text += f"MediaCurator is watching the following directories: "
text += '\n '.join(map(str, self.directories)) + '\n' text += '\n '.join(map(str, self.directories)) + '\n'
text += f"MediaCurator is tracking {len(self.videos)} video files" text += f"MediaCurator is tracking {len(self.videos)} video files"
return text return text
def load_directories(self, verbose = False): def load_directories(self, verbose=False):
''' '''
Scan folders for video files respecting the inputs requested by the user Scans folders for video files respecting the inputs requested by the user and saves them to the videos dictionary.
Save them to the videos dictionary
Args: Args:
verbose = False : A list of print options verbose (bool): A flag to enable verbose output.
Returns:
''' '''
print(f"{colorama.Fore.GREEN}Scanning files in {', '.join(map(str, self.directories))} for videos{colorama.Fore.RESET}") print(f"{cgreen}Scanning files in {', '.join(map(str, self.directories))} for videos{creset}")
videolist = [] videolist = []
for directory in self.directories: for directory in self.directories:
path = Path(directory) path = Path(directory)
# get all video filetypes # Get all video filetypes
if "wmv" in self.inputs or "any" in self.inputs or len(self.inputs) < 1: for ext in ["wmv", "avi", "mkv", "mp4", "m4v", "flv", "mpg", "mpeg", "vid", "vob", "divx", "ogm", "webm"]:
videolist += list(path.rglob("*.[wW][mM][vV]")) if ext in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
if "avi" in self.inputs or "any" in self.inputs or len(self.inputs) < 1: videolist += list(path.rglob(f"*.[{ext.upper()}{ext.lower()}]"))
videolist += list(path.rglob("*.[aA][vV][iI]"))
if "mkv" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[mM][kK][vV]"))
if "mp4" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[mM][pP]4"))
if "m4v" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[mM]4[vV]"))
if "flv" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[fF][lL][vV]"))
if "mpg" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[mM][pP][gG]"))
if "mpeg" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[mM][pP][eE][gG]"))
if "vid" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[vV][iI][dD]"))
if "vob" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[vV][oO][bB]"))
if "divx" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[dD][iI][vV][xX]"))
if "ogm" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[oO][gG][mM]"))
if "webm" in self.inputs or "any" in self.inputs or len(self.inputs) < 1:
videolist += list(path.rglob("*.[wW][eE][bB][mM]"))
# Remove folders # Remove folders
videolist_tmp = videolist videolist_tmp = videolist
videolist = [video for video in videolist_tmp if video.is_file()] videolist = [video for video in videolist_tmp if video.is_file()]
# Map it all to the videos dictionary as initiated Video objects # Map it all to the videos dictionary as initiated Video objects
print(f"{colorama.Fore.GREEN}Analazing {len(videolist)} videos in {', '.join(map(str, self.directories))}{colorama.Fore.RESET}") print(f"{cgreen}Analyzing {len(videolist)} videos in {', '.join(map(str, self.directories))}{creset}")
iteration = 0 iteration = 0
for video in videolist: for video in videolist:
if verbose: if verbose:
iteration += 1 iteration += 1
print(f'{int((iteration / len(videolist )* 100))}% complete', end='\r') print(f'{int((iteration / len(videolist) * 100))}% complete', end='\r')
self.videos[video] = Video(video, verbose = verbose) self.videos[video] = Video(video, verbose=verbose)
def filter_videos(self, verbose = False): def filter_videos(self, verbose=False):
''' Mark useless videos in the videos dictionary (default is useful) '''
Marks videos for operation in the videos dictionary (default is not to operate).
Args: Args:
verbose = False : A list of print options verbose (bool): A flag to enable verbose output.
Returns:
''' '''
print(f"{colorama.Fore.GREEN}Filtering {len(self.videos)} videos for the requested parameters{colorama.Fore.RESET}") print(f"{cgreen}Filtering {len(self.videos)} videos for the requested parameters{creset}")
for filepath in self.videos: for filepath in self.videos:
# filter for filetypes # Filter for filetypes
if len([filtr for filtr in self.filters if filtr in ["old", "mpeg4", "mpeg", "wmv3", "wmv", "h264", "hevc", "x265", "av1"]]) > 0: if len([filtr for filtr in self.filters if filtr in ["old", "mpeg4", "mpeg", "wmv3", "wmv", "h264", "hevc", "x265", "av1"]]) > 0:
useful = False operate = False
if "old" in self.filters and self.videos[filepath].codec not in ["hevc", "av1"]: if "old" in self.filters and self.videos[filepath].codec not in ["hevc", "av1"]:
useful = True operate = True
if ("mpeg4" in self.filters or "mpeg" in self.filters) and self.videos[filepath].codec in ["mpeg4", "msmpeg4v3"]: if ("mpeg4" in self.filters or "mpeg" in self.filters) and self.videos[filepath].codec in ["mpeg4", "msmpeg4v3"]:
useful = True operate = True
if "mpeg" in self.filters and self.videos[filepath].codec in ["mpeg1video"]: if "mpeg" in self.filters and self.videos[filepath].codec in ["mpeg1video"]:
useful = True operate = True
if ("wmv3" in self.filters or "wmv" in self.filters) and self.videos[filepath].codec in ["wmv3"]: if ("wmv3" in self.filters or "wmv" in self.filters) and self.videos[filepath].codec in ["wmv3"]:
useful = True operate = True
if "h264" in self.filters and self.videos[filepath].codec in ["h264"]: if "h264" in self.filters and self.videos[filepath].codec in ["h264"]:
useful = True operate = True
if ("hevc" in self.filters or "x265" in self.filters) and self.videos[filepath].codec in ["hevc"]: if ("hevc" in self.filters or "x265" in self.filters) and self.videos[filepath].codec in ["hevc"]:
useful = True operate = True
if "av1" in self.filters and self.videos[filepath].codec in ["av1"]: if "av1" in self.filters and self.videos[filepath].codec in ["av1"]:
useful = True operate = True
self.videos[filepath].useful = useful self.videos[filepath].operate = operate
# keep video if useful and user wants to also filter by selected resolutions # Keep video for operation if specified resolution
if self.videos[filepath].useful and len([filtr for filtr in self.filters if filtr in ["lowres", "hd", "subsd", "sd", "720p", "1080p", "uhd"]]) > 0: if self.videos[filepath].operate and len([filtr for filtr in self.filters if filtr in ["lowres", "hd", "subsd", "sd", "720p", "1080p", "uhd"]]) > 0:
useful = False operate = False
if "subsd" in self.filters and self.videos[filepath].definition in ["subsd"]: if "subsd" in self.filters and self.videos[filepath].definition in ["subsd"]:
useful = True operate = True
if "sd" in self.filters and self.videos[filepath].definition in ["sd"]: if "sd" in self.filters and self.videos[filepath].definition in ["sd"]:
useful = True operate = True
if "720p" in self.filters and self.videos[filepath].definition in ["720p"]: if "720p" in self.filters and self.videos[filepath].definition in ["720p"]:
useful = True operate = True
if "1080p" in self.filters and self.videos[filepath].definition in ["1080p"]: if "1080p" in self.filters and self.videos[filepath].definition in ["1080p"]:
useful = True operate = True
if "uhd" in self.filters and self.videos[filepath].definition in ["uhd"]: if "uhd" in self.filters and self.videos[filepath].definition in ["uhd"]:
useful = True operate = True
if "lowres" in self.filters and self.videos[filepath].definition in ["subsd", "sd"]: if "lowres" in self.filters and self.videos[filepath].definition in ["subsd", "sd"]:
useful = True operate = True
if "hd" in self.filters and self.videos[filepath].definition in ["720p", "1080p", "uhd"]: if "hd" in self.filters and self.videos[filepath].definition in ["720p", "1080p", "uhd"]:
useful = True operate = True
self.videos[filepath].useful = useful self.videos[filepath].operate = operate
# keep video if useful and user wants to also filter when there is an ffmpeg errors # Keep video for operation if ffmpeg error exists
if self.videos[filepath].useful and len([filtr for filtr in self.filters if filtr in ["fferror"]]) > 0: if self.videos[filepath].operate and len([filtr for filtr in self.filters if filtr in ["fferror"]]) > 0:
useful = False operate = False
if self.videos[filepath].error: if self.videos[filepath].error:
useful = True operate = True
self.videos[filepath].useful = useful self.videos[filepath].operate = operate
print(f"{cgreen}Found {len([filepath for filepath in self.videos if self.videos[filepath].operate])} videos for the requested parameters{creset}")
print(f"{colorama.Fore.GREEN}Found {len([filepath for filepath in self.videos if self.videos[filepath].useful])} videos for the requested parameters{colorama.Fore.RESET}") def unwatch(self, filepath, delete=False):
'''
def unwatch(self, filepath, delete = False): Removes a video from the index and deletes it if requested.
''' remove a video from the index and delete it if requested
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath of the video.
delete = False : Delete the file as well as removing it from the library delete (bool): If True, delete the file as well as removing it from the library.
Returns: Returns:
boolean : Operation success bool: True if operation successful, False otherwise.
''' '''
if delete: if delete:

View File

@ -10,14 +10,16 @@ import sys
import colorama import colorama
colorama.init() colorama.init()
cgreen = colorama.Fore.GREEN
cyellow = colorama.Fore.YELLOW
cred = colorama.Fore.RED
creset = colorama.Fore.RESET
def load_arguments(): def load_arguments():
'''Get/load command parameters '''Get/load command parameters
Args:
Returns: Returns:
arguments: A dictionary of lists of the options passed by the user dict: A dictionary containing lists of options passed by the user
''' '''
arguments = { arguments = {
"directories": list(), "directories": list(),
@ -29,12 +31,12 @@ def load_arguments():
} }
for arg in sys.argv: for arg in sys.argv:
# Confirm with the user that he selected to delete found files # Confirm with the user that they selected to delete found files
if "-del" in arg: if "-del" in arg:
print( print(
f"{colorama.Fore.YELLOW}WARNING: Delete option selected!{colorama.Fore.RESET}") f"{cyellow}WARNING: Delete option selected!{creset}")
if not user_confirm(f"Are you sure you wish to delete all found results after selected operations are succesfull ? [Y/N] ?", color="yellow"): if not user_confirm(f"Are you sure you wish to delete all found results after selected operations are successful? [Y/N] ?", color="yellow"):
print(f"{colorama.Fore.GREEN}Exiting!{colorama.Fore.RESET}") print(f"{cgreen}Exiting!{creset}")
exit() exit()
elif "-in:" in arg: elif "-in:" in arg:
arguments["inputs"] += arg[4:].split(",") arguments["inputs"] += arg[4:].split(",")
@ -53,19 +55,17 @@ def load_arguments():
def detect_ffmpeg(): def detect_ffmpeg():
'''Returns the version of ffmpeg that is installed or false '''Returns the version of ffmpeg that is installed or False
Args:
Returns: Returns:
String : The version number of the installed FFMPEG str: The version number of the installed FFMPEG
False : The failure of retreiving the version number False: If version retrieval failed
''' '''
try: try:
txt = subprocess.check_output( txt = subprocess.check_output(
['ffmpeg', '-version'], stderr=subprocess.STDOUT).decode() ['ffmpeg', '-version'], stderr=subprocess.STDOUT).decode()
if "ffmpeg version" in txt: if "ffmpeg version" in txt:
# Strip the useless text and # Strip the useless text
return txt.split(' ')[2] return txt.split(' ')[2]
except: except:
pass pass
@ -73,19 +73,19 @@ def detect_ffmpeg():
def user_confirm(question, color=False): def user_confirm(question, color=False):
'''Returns the user answer to a yes or no question '''Returns the user's answer to a yes or no question
Args: Args:
question : A String containing the user question question (str): The user question
color : A String containing the prefered color for a question (reg/yellow) color (str, optional): The preferred color for a question (red/yellow)
Returns: Returns:
Bool : Positive or negative return to the user question bool: True for a positive response, False otherwise
''' '''
if color == "yellow": if color == "yellow":
print(f"{colorama.Fore.YELLOW}{question} {colorama.Fore.RESET}", end='') print(f"{cyellow}{question} {creset}", end='')
answer = input() answer = input()
elif color == "red": elif color == "red":
print(f"{colorama.Fore.RED}{question} {colorama.Fore.RESET}", end='') print(f"{cred}{question} {creset}", end='')
answer = input() answer = input()
else: else:
answer = input(f"{question} ") answer = input(f"{question} ")
@ -98,32 +98,31 @@ def user_confirm(question, color=False):
def deletefile(filepath): def deletefile(filepath):
'''Delete a file, Returns a boolean '''Delete a file and return a boolean indicating success
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath
Returns: Returns:
Bool : The success of the operation bool: True if successful, False otherwise
''' '''
try: try:
os.remove(filepath) os.remove(filepath)
except OSError: except OSError:
print(f"{colorama.Fore.RED}Error deleting {filepath}{colorama.Fore.RESET}") print(f"{cred}Error deleting {filepath}{creset}")
return False return False
print(f"{colorama.Fore.GREEN}Successfully deleted {filepath}{colorama.Fore.RESET}") print(f"{cgreen}Successfully deleted {filepath}{creset}")
return True return True
def findfreename(filepath, attempt=0): def findfreename(filepath, attempt=0):
''' Given a filepath it will try to find a free filename by appending to the name. '''Find a free filename by appending [HEVC] or [HEVC](#) to the name if necessary
First trying as passed in argument, then adding [HEVC] to the end and if all fail [HEVC](#).
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath
attempt : The number of times we have already tryed attempt (int, optional): The number of attempts made
Returns: Returns:
filepath : The first free filepath we found str: The first free filepath found
''' '''
attempt += 1 attempt += 1
filename = str(filepath)[:str(filepath).rindex(".")] filename = str(filepath)[:str(filepath).rindex(".")]

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
'''Its a video!''' '''This module defines the Video class, which contains information and methods for video files.'''
from .tools import deletefile, findfreename from .tools import deletefile, findfreename
import subprocess import subprocess
@ -8,71 +8,73 @@ import os
import colorama import colorama
colorama.init() colorama.init()
cyellow = colorama.Fore.YELLOW
cred = colorama.Fore.RED
creset = colorama.Fore.RESET
class Video(): class Video():
'''Contains the information and methods of a video file.''' '''Contains the information and methods of a video file.'''
def __init__(self, filepath, useful = True, verbose = False): def __init__(self, filepath, operate=True, verbose=False):
'''Contains the information and methods of a video file. '''Initializes a Video instance with provided parameters.
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath of the video.
useful : A boolean marking if the video is to be operated on operate (bool): A flag indicating if the video is to be operated on.
verbose = False : A list of print options verbose (bool): A flag indicating whether to enable verbose output.
Returns:
''' '''
# Initialize attributes
self.path = None self.path = None
self.filename_origin = None self.filename_origin = None
self.filesize = None self.filesize = None
self.filename_new = None self.filename_new = None
self.filename_tmp = None self.filename_tmp = None
self.useful = None self.operate = None
self.codec = None self.codec = None
self.error = None self.error = None
self.definition = None self.definition = None
self.width = None self.width = None
self.height = None self.height = None
#Breaking down the full path in its components # Break down the full path into its components
if os.name == 'nt': if os.name == 'nt':
self.path = str(filepath)[:str(filepath).rindex("\\") + 1] self.path = str(filepath)[:str(filepath).rindex("\\") + 1]
self.filename_origin = str(filepath)[str(filepath).rindex("\\") + 1:] self.filename_origin = str(filepath)[str(filepath).rindex("\\") + 1:]
else: else:
self.path = str(filepath)[:str(filepath).rindex("/") + 1] self.path = str(filepath)[:str(filepath).rindex("/") + 1]
self.filename_origin = str(filepath)[str(filepath).rindex("/") + 1:] self.filename_origin = str(filepath)[str(filepath).rindex("/") + 1:]
if not os.path.exists(filepath): if not os.path.exists(filepath):
self.error = f"FileNotFoundError: [Errno 2] No such file or directory: '{filepath}'" self.error = f"FileNotFoundError: [Errno 2] No such file or directory: '{filepath}'"
self.useful = useful self.operate = operate
else: else:
# Marking useful is user manually set it. # Mark the video for operation if specified manually by the user
self.useful = useful self.operate = operate
#Gathering information on the video # Gather information on the video
self.filesize = self.detect_filesize(filepath) self.filesize = self.detect_filesize(filepath)
self.error = self.detect_fferror(filepath) self.error = self.detect_fferror(filepath)
self.codec = self.detect_codec(filepath) self.codec = self.detect_codec(filepath)
try: try:
self.width, self.height = self.detect_resolution(filepath) self.width, self.height = self.detect_resolution(filepath)
self.definition = self.detect_definition( self.definition = self.detect_definition(
width = self.width, width=self.width,
height = self.height ) height=self.height
)
except: except:
self.width, self.height = False, False self.width, self.height = False, False
self.definition = False self.definition = False
if self.error and verbose: if self.error and verbose:
print(f"{colorama.Fore.RED}There seams to be an error with \"{filepath}\"{colorama.Fore.RESET}") print(f"{cred}There seems to be an error with \"{filepath}\"{creset}")
print(f"{colorama.Fore.RED} {self.error}{colorama.Fore.RESET}") print(f"{cred} {self.error}{creset}")
def __str__(self): def __str__(self):
'''Returns a short formated string about the video '''Returns a short formatted string about the video.
Args:
Returns: Returns:
String : A short formated string about the video str: A short formatted string about the video.
''' '''
if type(self.error) is str and "FileNotFoundError" in self.error: if type(self.error) is str and "FileNotFoundError" in self.error:
@ -80,49 +82,41 @@ class Video():
text = f"{self.codec} - " text = f"{self.codec} - "
# If the first character of the definition is not a number (ie UHD and not 720p) upper it # If the first character of the definition is not a number (e.g., UHD and not 720p), upper it
if self.definition and self.definition[0] and not self.definition[0].isnumeric(): if self.definition and self.definition[0] and not self.definition[0].isnumeric():
text += f"{self.definition.upper()}: ({self.width}x{self.height}) - " text += f"{self.definition.upper()}: ({self.width}x{self.height}) - "
else: else:
text += f"{self.definition}: ({self.width}x{self.height}) - " text += f"{self.definition}: ({self.width}x{self.height}) - "
# Return the size in mb or gb if more than 1024 mb # Return the size in MB or GB if more than 1024 MB
if self.filesize >= 1024: if self.filesize >= 1024:
text += f"{self.filesize / 1024 :.2f} gb - " text += f"{self.filesize / 1024 :.2f} GB - "
else: else:
text += f"{self.filesize} mb - " text += f"{self.filesize} MB - "
text += f"'{self.path + self.filename_origin}'" text += f"'{self.path + self.filename_origin}'"
if self.error: if self.error:
text += f"{colorama.Fore.RED}\nErrors:{colorama.Fore.RESET}" text += f"{cred}\nErrors:{creset}"
for err in self.error.splitlines(): for err in self.error.splitlines():
text += f"{colorama.Fore.RED}\n {err}{colorama.Fore.RESET}" text += f"{cred}\n {err}{creset}"
return text return text
__repr__ = __str__ __repr__ = __str__
def fprint(self): def fprint(self):
'''Returns a long formated string about the video '''Returns a long formatted string about the video.
Args:
Returns: Returns:
String : A long formated string about the video str: A long formatted string about the video.
''' '''
if type(self.error) is str and "FileNotFoundError" in self.error: if type(self.error) is str and "FileNotFoundError" in self.error:
return self.error return self.error
text = f"{self.path + self.filename_origin}\n" text = f"{self.path + self.filename_origin}\n"
#text += f" Useful: {self.useful}\n"
# If the first character of the definition is not a number (ie UHD and not 720p) upper it
if self.definition and self.definition[0] and not self.definition[0].isnumeric(): if self.definition and self.definition[0] and not self.definition[0].isnumeric():
text += f" Definition: {self.definition.upper()}: ({self.width}x{self.height})\n" text += f" Definition: {self.definition.upper()}: ({self.width}x{self.height})\n"
else: else:
@ -130,34 +124,31 @@ class Video():
text += f" Codec: {self.codec}\n" text += f" Codec: {self.codec}\n"
# Return the size in mb or gb if more than 1024 mb # Return the size in MB or GB if more than 1024 MB
if self.filesize >= 1024: if self.filesize >= 1024:
text += f" size: {self.filesize / 1024 :.2f} gb" text += f" Size: {self.filesize / 1024 :.2f} GB"
else: else:
text += f" size: {self.filesize} mb" text += f" Size: {self.filesize} MB"
if self.error: if self.error:
text += f"{colorama.Fore.RED}\n Errors:{colorama.Fore.RESET}" text += f"{cred}\n Errors:{creset}"
for err in self.error.splitlines(): for err in self.error.splitlines():
text += f"{colorama.Fore.RED}\n {err}{colorama.Fore.RESET}" text += f"{cred}\n {err}{creset}"
return text return text
def convert(self, vcodec="x265", acodec=False, extension="mkv", verbose=False):
def convert(self, vcodec = "x265", acodec = False, extension = "mkv", verbose = False):
''' '''
Convert to original file to the requested format / codec Converts the original file to the requested format / codec.
verbose will print ffmpeg's output
Args: Args:
vcodec = "x265" : The new video codec, supports av1 or x265 vcodec (str): The new video codec, supports av1 or x265.
acodec = False : Currently not enabled, will be for audio codecs acodec (bool): Currently not enabled, will be for audio codecs.
extension = "mkv" : A string containing the new video container format and file extention extension (str): The new video container format and file extension.
verbose = False : A boolean enabling verbosity verbose (bool): A flag enabling verbosity.
Returns: Returns:
boolean : Operation success bool: True if operation successful, False otherwise.
''' '''
# Setting new filename # Setting new filename
@ -178,18 +169,18 @@ class Video():
else: else:
newfilename = str(newfilename)[str(newfilename).rindex("/") + 1:] newfilename = str(newfilename)[str(newfilename).rindex("/") + 1:]
self.filename_tmp = newfilename self.filename_tmp = newfilename
# Settting ffmpeg # Setting ffmpeg
args = ['ffmpeg', '-i', self.path + self.filename_origin] args = ['ffmpeg', '-i', self.path + self.filename_origin]
# conversion options # Conversion options
if vcodec == "av1": if vcodec == "av1":
args += ['-c:v', 'libaom-av1', '-strict', 'experimental'] args += ['-c:v', 'libaom-av1', '-strict', 'experimental']
elif vcodec == "x265" or vcodec == "hevc": elif vcodec == "x265" or vcodec == "hevc":
args += ['-c:v', 'libx265'] args += ['-c:v', 'libx265']
args += ['-max_muxing_queue_size', '1000'] args += ['-max_muxing_queue_size', '1000']
# conversion output # Conversion output
args += [self.path + self.filename_tmp] args += [self.path + self.filename_tmp]
try: try:
@ -200,10 +191,10 @@ class Video():
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
deletefile(self.path + self.filename_tmp) deletefile(self.path + self.filename_tmp)
self.filename_tmp = "" self.filename_tmp = ""
print(f"{colorama.Fore.RED}Conversion failed {e}{colorama.Fore.RESET}") print(f"{cred}Conversion failed {e}{creset}")
return False return False
except KeyboardInterrupt: except KeyboardInterrupt:
print(f"{colorama.Fore.YELLOW}Conversion cancelled, cleaning up...{colorama.Fore.RESET}") print(f"{cyellow}Conversion cancelled, cleaning up...{creset}")
deletefile(self.path + self.filename_tmp) deletefile(self.path + self.filename_tmp)
self.filename_tmp = "" self.filename_tmp = ""
exit() exit()
@ -211,89 +202,89 @@ class Video():
try: try:
os.chmod(f"{self.path}{self.filename_tmp}", 0o777) os.chmod(f"{self.path}{self.filename_tmp}", 0o777)
except PermissionError: except PermissionError:
print(f"{colorama.Fore.RED}PermissionError on: '{self.path}{self.filename_tmp}'{colorama.Fore.RESET}") print(f"{cred}PermissionError on: '{self.path}{self.filename_tmp}'{creset}")
self.filename_new = self.filename_tmp self.filename_new = self.filename_tmp
self.filename_tmp = "" self.filename_tmp = ""
return True return True
@staticmethod @staticmethod
def detect_fferror(filepath): def detect_fferror(filepath):
'''Returns a string with the detected errors '''Returns a string with the detected errors.
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath of the video.
Returns: Returns:
String : The errors that have been found / happened str: The errors that have been found/happened.
False : The lack of errors False: The lack of errors.
''' '''
try: try:
args = ["ffprobe","-v","error",str(filepath)] args = ["ffprobe", "-v", "error", str(filepath)]
output = subprocess.check_output(args, stderr=subprocess.STDOUT) output = subprocess.check_output(args, stderr=subprocess.STDOUT)
output = output.decode().strip() output = output.decode().strip()
if len(output) > 0: if len(output) > 0:
return output return output
except (subprocess.CalledProcessError, IndexError): except (subprocess.CalledProcessError, IndexError):
return f'{colorama.Fore.RED}There seams to be a "subprocess.CalledProcessError" error with \"{filepath}\"{colorama.Fore.RESET}' return f'{cred}There seems to be a "subprocess.CalledProcessError" error with \"{filepath}\"{creset}'
return False return False
@staticmethod @staticmethod
def detect_codec(filepath): def detect_codec(filepath):
'''Returns a string with the detected codec '''Returns a string with the detected codec.
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath of the video.
Returns: Returns:
String : The codec that has been detected str: The codec that has been detected.
False : An error in the codec fetching process False: An error in the codec fetching process.
''' '''
output = False output = False
try: try:
args = ["ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "default=noprint_wrappers=1:nokey=1", str(filepath)] args = ["ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "default=noprint_wrappers=1:nokey=1", str(filepath)]
output = subprocess.check_output(args, stderr=subprocess.STDOUT) output = subprocess.check_output(args, stderr=subprocess.STDOUT)
# Decoding from binary, stripping whitespace, keep only last line
# decoding from binary, stripping whitespace, keep only last line # in case ffprobe added error messages over the requested information
# in case ffmprobe added error messages over the requested information
output = output.decode().strip() output = output.decode().strip()
except (subprocess.CalledProcessError, IndexError): except (subprocess.CalledProcessError, IndexError):
return False return False
return output return output
@staticmethod @staticmethod
def detect_resolution(filepath): def detect_resolution(filepath):
'''Returns a list with the detected width(0) and height(1) '''Returns a list with the detected width(0) and height(1).
Args: Args:
filepath : A string containing the full filepath filepath (str): The full filepath of the video.
Returns: Returns:
List : the detected width(0) and height(1) List: The detected width(0) and height(1).
False : An error in the codec fetching process False: An error in the resolution fetching process.
''' '''
try: try:
args = ["ffprobe","-v","quiet","-select_streams","v:0", "-show_entries","stream=width,height","-of","csv=s=x:p=0",str(filepath)] args = ["ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=s=x:p=0", str(filepath)]
output = subprocess.check_output(args, stderr=subprocess.STDOUT) output = subprocess.check_output(args, stderr=subprocess.STDOUT)
# Decoding from binary, stripping whitespace, keep only last line
# decoding from binary, stripping whitespace, keep only last line # in case ffprobe added error messages over the requested information
# in case ffmprobe added error messages over the requested information
output = output.decode().strip() output = output.decode().strip()
# See if we got convertible data
# See if we got convertable data
output = [int(output.split("x")[0]), int(output.split("x")[1])] output = [int(output.split("x")[0]), int(output.split("x")[1])]
except (subprocess.CalledProcessError, IndexError): except (subprocess.CalledProcessError, IndexError):
return False return False
return output[0], output[1] return output[0], output[1]
@staticmethod @staticmethod
def detect_definition(filepath = False, width = False, height = False): def detect_definition(filepath=False, width=False, height=False):
'''Returns a string with the detected definition corrected for dead space '''Returns a string with the detected definition corrected for dead space.
Args: Args:
filepath : A string containing the full filepath filepath (str): A string containing the full filepath.
width (int): The width of the video.
height (int): The height of the video.
Returns: Returns:
List : The classified definition in width(0) and height(1) str: The classified definition in width(0) and height(1).
False : An error in the process False: An error in the process.
''' '''
if filepath: if filepath:
width, height = Video.detect_resolution(filepath) width, height = Video.detect_resolution(filepath)
@ -312,13 +303,14 @@ class Video():
@staticmethod @staticmethod
def detect_filesize(filepath): def detect_filesize(filepath):
'''Returns an integer with size in mb '''Returns an integer with size in MB.
Args: Args:
filepath : A string containing the full filepath filepath (str): A string containing the full filepath.
Returns: Returns:
Integer : The filesize in mb int: The filesize in MB.
False : An error in the process False: An error in the process.
''' '''
try: try:
size = int(os.path.getsize(filepath) / 1024 / 1024) size = int(os.path.getsize(filepath) / 1024 / 1024)

View File

@ -22,13 +22,20 @@ except ModuleNotFoundError:
from library.medialibrary import MediaLibrary from library.medialibrary import MediaLibrary
from library.tools import detect_ffmpeg, user_confirm, load_arguments from library.tools import detect_ffmpeg, user_confirm, load_arguments
# Import colorama for colored output
import colorama import colorama
colorama.init() colorama.init()
# Define color codes for colored output
ccyan = colorama.Fore.CYAN
cblue = colorama.Fore.BLUE
cgreen = colorama.Fore.GREEN
cred = colorama.Fore.RED
creset = colorama.Fore.RESET
def main(): def main():
''' '''
MediaCurator's main function MediaCurator's main function
Args: Args:
@ -39,14 +46,14 @@ def main():
# confirm that the command has enough parameters # confirm that the command has enough parameters
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(f"{colorama.Fore.RED}ERROR: Command not understood, please see documentation.{colorama.Fore.RESET}") print(f"{cred}ERROR: Command not understood, please see documentation.{creset}")
# confirm that ffmpeg in indeed installed # confirm that ffmpeg in indeed installed
ffmpeg_version = detect_ffmpeg() ffmpeg_version = detect_ffmpeg()
if not ffmpeg_version: if not ffmpeg_version:
print(f"{colorama.Fore.RED}No ffmpeg version detected{colorama.Fore.RESET}") print(f"{cred}No ffmpeg version detected{creset}")
exit() exit()
print(f"{colorama.Fore.BLUE}ffmpeg version detected: {ffmpeg_version}{colorama.Fore.RESET}") print(f"{cblue}ffmpeg version detected: {ffmpeg_version}{creset}")
# Get/load command parameters # Get/load command parameters
arguments = load_arguments() arguments = load_arguments()
@ -57,27 +64,26 @@ def main():
elif len(arguments["directories"]) > 0: elif len(arguments["directories"]) > 0:
medialibrary = MediaLibrary(directories = arguments["directories"], inputs = arguments["inputs"], filters = arguments["filters"]) medialibrary = MediaLibrary(directories = arguments["directories"], inputs = arguments["inputs"], filters = arguments["filters"])
else: else:
print(f"{colorama.Fore.RED}ERROR: No files or directories selected.{colorama.Fore.RESET}") print(f"{cred}ERROR: No files or directories selected.{creset}")
return return
# Actions # Actions
if sys.argv[1] == "list": if sys.argv[1] == "list":
# Pulling list of marked videos / original keys for the medialibrary.videos dictionary # Pulling list of marked videos / original keys for the medialibrary.videos dictionary
keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].useful] keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].operate]
keylist.sort() keylist.sort()
for filepath in keylist: for filepath in keylist:
if medialibrary.videos[filepath].useful: if medialibrary.videos[filepath].operate:
if "formated" in arguments["printop"] or "verbose" in arguments["printop"]: if "formated" in arguments["printop"] or "verbose" in arguments["printop"]:
if medialibrary.videos[filepath].error: if medialibrary.videos[filepath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[filepath].fprint()}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[filepath].fprint()}{creset}")
else: else:
print(medialibrary.videos[filepath].fprint()) print(medialibrary.videos[filepath].fprint())
else: else:
if medialibrary.videos[filepath].error: if medialibrary.videos[filepath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[filepath]}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[filepath]}{creset}")
else: else:
print(medialibrary.videos[filepath]) print(medialibrary.videos[filepath])
@ -85,21 +91,20 @@ def main():
if "-del" in sys.argv: if "-del" in sys.argv:
medialibrary.unwatch(filepath, delete = True) medialibrary.unwatch(filepath, delete = True)
elif sys.argv[1] == "test": elif sys.argv[1] == "test":
# Pulling list of marked videos / original keys for the medialibrary.videos dictionary # Pulling list of marked videos / original keys for the medialibrary.videos dictionary
keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].useful] keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].operate]
keylist.sort() keylist.sort()
for filepath in keylist: for filepath in keylist:
if medialibrary.videos[filepath].useful: if medialibrary.videos[filepath].operate:
if "formated" in arguments["printop"] or "verbose" in arguments["printop"]: if "formated" in arguments["printop"] or "verbose" in arguments["printop"]:
if medialibrary.videos[filepath].error: if medialibrary.videos[filepath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[filepath].fprint()}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[filepath].fprint()}{creset}")
else: else:
print(medialibrary.videos[filepath].fprint()) print(medialibrary.videos[filepath].fprint())
else: else:
if medialibrary.videos[filepath].error: if medialibrary.videos[filepath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[filepath]}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[filepath]}{creset}")
else: else:
print(medialibrary.videos[filepath]) print(medialibrary.videos[filepath])
@ -111,7 +116,7 @@ def main():
counter = 0 counter = 0
# Pulling list of marked videos / original keys for the medialibrary.videos dictionary # Pulling list of marked videos / original keys for the medialibrary.videos dictionary
keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].useful] keylist = [filepath for filepath in medialibrary.videos if medialibrary.videos[filepath].operate]
keylist.sort() keylist.sort()
for filepath in keylist: for filepath in keylist:
@ -123,19 +128,19 @@ def main():
vcodec = "x265" vcodec = "x265"
# Verbosing # Verbosing
print(f"{colorama.Fore.GREEN}****** Starting conversion {counter} of {len(keylist)}: '{colorama.Fore.CYAN}{medialibrary.videos[filepath].filename_origin}{colorama.Fore.GREEN}' from {colorama.Fore.CYAN}{medialibrary.videos[filepath].codec}{colorama.Fore.GREEN} to {colorama.Fore.CYAN}{vcodec}{colorama.Fore.GREEN}...{colorama.Fore.RESET}") print(f"{cgreen}****** Starting conversion {counter} of {len(keylist)}: '{ccyan}{medialibrary.videos[filepath].filename_origin}{cgreen}' from {ccyan}{medialibrary.videos[filepath].codec}{cgreen} to {ccyan}{vcodec}{cgreen}...{creset}")
print(f"{colorama.Fore.CYAN}Original file:{colorama.Fore.RESET}") print(f"{ccyan}Original file:{creset}")
if "formated" in arguments["printop"] or "verbose" in arguments["printop"]: if "formated" in arguments["printop"] or "verbose" in arguments["printop"]:
print(medialibrary.videos[filepath].fprint()) print(medialibrary.videos[filepath].fprint())
else: else:
print(medialibrary.videos[filepath]) print(medialibrary.videos[filepath])
print(f"{colorama.Fore.GREEN}Converting please wait...{colorama.Fore.RESET}", end="\r") print(f"{cgreen}Converting please wait...{creset}", end="\r")
# Converting # Converting
if medialibrary.videos[filepath].convert(verbose = "verbose" in arguments["printop"]): if medialibrary.videos[filepath].convert(verbose = "verbose" in arguments["printop"]):
# Mark the job as done # Mark the job as done
medialibrary.videos[filepath].useful = False medialibrary.videos[filepath].operate = False
# Scan the new video # Scan the new video
newfpath = medialibrary.videos[filepath].path + medialibrary.videos[filepath].filename_new newfpath = medialibrary.videos[filepath].path + medialibrary.videos[filepath].filename_new
@ -143,17 +148,16 @@ def main():
medialibrary.videos[newfpath] = Video(newfpath, verbose = "verbose" in arguments["printop"]) medialibrary.videos[newfpath] = Video(newfpath, verbose = "verbose" in arguments["printop"])
# Verbose # Verbose
print(f"{colorama.Fore.GREEN}Successfully converted '{medialibrary.videos[filepath].filename_origin}'{colorama.Fore.CYAN}({medialibrary.videos[filepath].filesize}mb){colorama.Fore.GREEN} to '{medialibrary.videos[newfpath].filename_origin}'{colorama.Fore.CYAN}({medialibrary.videos[newfpath].filesize}mb){colorama.Fore.GREEN}, {colorama.Fore.CYAN}new file:{colorama.Fore.RESET}") print(f"{cgreen}Successfully converted '{medialibrary.videos[filepath].filename_origin}'{ccyan}({medialibrary.videos[filepath].filesize}mb){cgreen} to '{medialibrary.videos[newfpath].filename_origin}'{ccyan}({medialibrary.videos[newfpath].filesize}mb){cgreen}, {ccyan}new file:{creset}")
if "formated" in arguments["printop"] or "verbose" in arguments["printop"]: if "formated" in arguments["printop"] or "verbose" in arguments["printop"]:
if medialibrary.videos[newfpath].error: if medialibrary.videos[newfpath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[newfpath].fprint()}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[newfpath].fprint()}{creset}")
else: else:
print(medialibrary.videos[newfpath].fprint()) print(medialibrary.videos[newfpath].fprint())
else: else:
if medialibrary.videos[newfpath].error: if medialibrary.videos[newfpath].error:
print(f"{colorama.Fore.RED}{medialibrary.videos[newfpath]}{colorama.Fore.RESET}") print(f"{cred}{medialibrary.videos[newfpath]}{creset}")
else: else:
print(medialibrary.videos[newfpath]) print(medialibrary.videos[newfpath])
@ -161,9 +165,5 @@ def main():
if "-del" in sys.argv: if "-del" in sys.argv:
medialibrary.unwatch(filepath, delete = True) medialibrary.unwatch(filepath, delete = True)
if __name__ == '__main__': if __name__ == '__main__':
main() main()