From df20bd363685700213942e5f5cebf02776b154a0 Mon Sep 17 00:00:00 2001 From: OleSTEEP Date: Sat, 19 Oct 2024 01:45:35 +0300 Subject: [PATCH] vnrecode: docstrings --- vnrecode/__main__.py | 4 +++ vnrecode/application.py | 11 ++++++- vnrecode/compress.py | 52 +++++++++++++++++++++++++++++++-- vnrecode/params.py | 12 ++++++++ vnrecode/printer.py | 57 ++++++++++++++++++++++++++++++++---- vnrecode/utils.py | 64 ++++++++++++++++++++++++++++++++--------- 6 files changed, 177 insertions(+), 23 deletions(-) diff --git a/vnrecode/__main__.py b/vnrecode/__main__.py index faf1839..fe45306 100644 --- a/vnrecode/__main__.py +++ b/vnrecode/__main__.py @@ -7,6 +7,10 @@ from .utils import Utils def init(): + """ + This function creates all needed class instances and run utility + :return: None + """ params = Params.setup() printer = Printer(params.source) utils = Utils(params, printer) diff --git a/vnrecode/application.py b/vnrecode/application.py index 3b6843e..c43102b 100755 --- a/vnrecode/application.py +++ b/vnrecode/application.py @@ -12,6 +12,9 @@ from .utils import Utils class Application: + """ + Main class for utility + """ def __init__(self, params_inst: Params, compress_inst: Compress, printer_inst: Printer, utils_inst: Utils): self.__params = params_inst @@ -20,6 +23,12 @@ class Application: self.__utils = utils_inst def run(self): + """ + Method creates a folder in which all the recoded files will be placed, + creates a queue of recoding processes for each file and, when the files are run out in the original folder, + calls functions to display the result + :return: None + """ start_time = datetime.now() self.__printer.win_ascii_esc() @@ -45,6 +54,6 @@ class Application: future.result() self.__utils.print_duplicates() - self.__utils.get_compression_status() + self.__utils.get_recode_status() self.__utils.sys_pause() print(f"Time taken: {datetime.now() - start_time}") \ No newline at end of file diff --git a/vnrecode/compress.py b/vnrecode/compress.py index 7b06988..4050e4c 100644 --- a/vnrecode/compress.py +++ b/vnrecode/compress.py @@ -9,10 +9,17 @@ from .utils import Utils class File: + """ + Class contains some methods to work with files + """ @staticmethod - def get_type(filename: Path) -> str: - + def get_type(path: Path) -> str: + """ + Method returns filetype string for file + :param path: Path of file to determine type + :return: filetype string: audio, image, video, unknown + """ extensions = { "audio": ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.opus', '.raw', '.wav', '.wma'], "image": ['.apng', '.avif', '.bmp', '.tga', '.tiff', '.dds', '.svg', '.webp', '.jpg', '.jpeg', '.png'], @@ -21,12 +28,17 @@ class File: } for file_type in extensions: - if filename.suffix in extensions[file_type]: + if path.suffix in extensions[file_type]: return file_type return "unknown" @staticmethod def has_transparency(img: Image) -> bool: + """ + Method checks if image has transparency + :param img: Pillow Image + :return: bool + """ if img.info.get("transparency", None) is not None: return True if img.mode == "P": @@ -49,6 +61,13 @@ class Compress: self.__utils = utils_inst def audio(self, input_path: Path, output_dir: Path, extension: str) -> Path: + """ + Method recodes audio files to another format using ffmpeg utility + :param input_path: Path of the original audio file + :param output_dir: Path of the output (compression) folder + :param extension: Extension of the new audio file + :return: Path of compressed audio file with md5 hash as prefix + """ bit_rate = self.__params.audio_bitrate prefix = self.__utils.get_hash(input_path.name) out_file = Path(output_dir, f'{prefix}_{input_path.stem}.{extension}') @@ -65,6 +84,13 @@ class Compress: return out_file def image(self, input_path: Path, output_dir: Path, extension: str) -> Path: + """ + Method recodes image files to another format using Pillow + :param input_path: Path of the original image file + :param output_dir: Path of the output (compression) folder + :param extension: Extension of the new image file + :return: Path of compressed image file with md5 hash as prefix + """ quality = self.__params.image_quality prefix = self.__utils.get_hash(input_path.name) out_file = Path(output_dir, f"{prefix}_{input_path.stem}.{extension}") @@ -97,6 +123,13 @@ class Compress: return out_file def video(self, input_path: Path, output_dir: Path, extension: str) -> Path: + """ + Method recodes video files to another format using ffmpeg utility + :param input_path: Path of the original video file + :param output_dir: Path of the output (compression) folder + :param extension: Extension of the new video file + :return: Path of compressed video file with md5 hash as prefix + """ prefix = self.__utils.get_hash(input_path.name) out_file = Path(output_dir, f'{prefix}_{input_path.stem}.{extension}') if not self.__params.video_skip: @@ -119,6 +152,13 @@ class Compress: return out_file def unknown(self, input_path: Path, output_dir: Path) -> Path: + """ + Method recodes files with "unknown" file format using ffmpeg, + in the hope that ffmpeg supports this file type and the default settings for it will reduce its size + :param input_path: Path of the original file + :param output_dir: Path of the output (compression) folder + :return: Path of compressed file with md5 hash as prefix + """ prefix = self.__utils.get_hash(input_path.name) out_file = Path(output_dir, f"{prefix}_{input_path.name}") if self.__params.force_compress: @@ -136,6 +176,12 @@ class Compress: return out_file def compress(self, source: Path, output: Path): + """ + It the core method for this program. Method determines file type and call compress function for it + :param source: Path of file to compress + :param output: Path of output file + :return: None + """ match File.get_type(source): case "audio": out_file = self.audio(source, output, self.__params.audio_ext) diff --git a/vnrecode/params.py b/vnrecode/params.py index d4db410..d0388f2 100644 --- a/vnrecode/params.py +++ b/vnrecode/params.py @@ -7,6 +7,10 @@ import tomllib @dataclass class Params: + """ + This dataclass contains all parameters for utility + """ + copy_unprocessed: bool force_compress: bool mimic_mode: bool @@ -33,6 +37,10 @@ class Params: @classmethod def setup(cls) -> Self: + """ + Method initialize all parameters and returns class instance + :return: Params instance + """ args = cls.get_args() if args.config is not None: if Path(args.config).is_file(): @@ -71,6 +79,10 @@ class Params: @staticmethod def get_args() -> Namespace: + """ + Method gets CLI arguments and returns argparse.Namespace instance + :return: argparse.Namespace of CLI args + """ parser = ArgumentParser(prog="vnrecode", description="Python utility to compress Visual Novel Resources" ) diff --git a/vnrecode/printer.py b/vnrecode/printer.py index 4de4f3c..a6046f9 100644 --- a/vnrecode/printer.py +++ b/vnrecode/printer.py @@ -5,40 +5,87 @@ import sys import os class Printer: + """ + Class implements CLI UI for this utility + """ - def __init__(self, folder: Path): + def __init__(self, source: Path): + """ + :param source: Path of original (compressing) folder to count its files for progress bar + """ file_count = 0 - for folder, folders, file in os.walk(folder): + for folder, folders, file in os.walk(source): file_count += len(file) self.bar = IncrementalBar('Compressing', max=file_count, suffix='[%(index)d/%(max)d] (%(percent).1f%%)') self.bar.update() - # Fill whole string with spaces for cleaning progress bar @staticmethod def clean_str(string: str) -> str: + """ + Method fills end of string with spaces to remove progress bar garbage from console + :param string: String to "clean" + :return: "Clean" string + """ return string + " " * (os.get_terminal_size().columns - len(string)) @staticmethod def win_ascii_esc(): + """ + Method setups colorama for cmd + :return: None + """ if sys.platform == "win32": colorama.init() def bar_print(self, string: str): + """ + Method prints some string in console and updates progress bar + :param string: String to print + :return: None + """ print(string) self.bar.update() def info(self, string: str): + """ + Method prints string with decor for info messages + :param string: String to print + :return: None + """ self.bar_print(self.clean_str(f"\r\033[100m- {string}\033[49m")) def warning(self, string: str): + """ + Method prints string with decor for warning messages + :param string: String to print + :return: None + """ self.bar_print(self.clean_str(f"\r\033[93m!\033[0m {string}\033[49m")) def error(self, string: str): + """ + Method prints string with decor for error messages + :param string: String to print + :return: None + """ self.bar_print(self.clean_str(f"\r\033[31m\u2715\033[0m {string}\033[49m")) def files(self, source_path: Path, output_path: Path, comment: str): + """ + Method prints the result of recoding a file with some decorations in the form: + input file name -> output file name (quality setting) + :param source_path: Input file Path + :param output_path: Output file Path + :param comment: Comment about recode quality setting + :return: None + """ self.bar_print(self.clean_str(f"\r\033[0;32m\u2713\033[0m \033[0;37m{source_path.stem}\033[0m{source_path.suffix}\033[0;37m -> " f"{source_path.stem}\033[0m{output_path.suffix}\033[0;37m ({comment})\033[0m")) - def unknown_file(self, file: str): - self.bar_print(self.clean_str(f"\r* \033[0;33m{file}\033[0m (File will be force compressed via ffmpeg)")) + def unknown_file(self, filename: str): + """ + Method prints the result of recoding unknown file + :param filename: Name of unknown file + :return: + """ + self.bar_print(self.clean_str(f"\r\u2713 \033[0;33m{filename}\033[0m (File will be force compressed via ffmpeg)")) diff --git a/vnrecode/utils.py b/vnrecode/utils.py index 2b52f30..7da06d3 100644 --- a/vnrecode/utils.py +++ b/vnrecode/utils.py @@ -9,6 +9,9 @@ from vnrecode.params import Params class Utils: + """ + Class contains various methods for internal utility use + """ def __init__(self, params_inst: Params, printer_inst: Printer): self.__errors = 0 @@ -18,24 +21,27 @@ class Utils: @staticmethod def sys_pause(): + """ + Method calls pause for Windows cmd shell + :return: None + """ if sys.platform == "win32": os.system("pause") - @staticmethod - def get_size(directory: Path) -> int: - total_size = 0 - for folder, folders, files in os.walk(directory): - for file in files: - path = Path(folder, file) - if not path.is_symlink(): - total_size += path.stat().st_size - return total_size - @staticmethod def get_hash(filename: str) -> str: + """ + Method returns 8 chars of md5 hash for filename + :param filename: File name to get md5 + :return: 8 chars of md5 hash + """ return hashlib.md5(filename.encode()).hexdigest()[:8] - def get_compression_status(self): + def get_recode_status(self): + """ + Method prints recoding results + :return: None + """ source_len = 0 output_len = 0 @@ -55,8 +61,8 @@ class Utils: else: self.__printer.warning("Original and compressed folders are not identical!") try: - source = self.get_size(self.__params.source) - output = self.get_size(self.__params.dest) + source = sum(file.stat().st_size for file in self.__params.source.glob('**/*') if file.is_file()) + output = sum(file.stat().st_size for file in self.__params.dest.glob('**/*') if file.is_file()) print(f"\nResult: {source/1024/1024:.2f}MB -> " f"{output/1024/1024:.2f}MB ({(output - source)/1024/1024:.2f}MB)") @@ -64,24 +70,48 @@ class Utils: self.__printer.warning("Nothing compressed!") def catch_unprocessed(self, input_path: Path, output_path: Path, error): + """ + Method processes files that have not been recoded due to an error and prints error to console + if hide_errors parameter is False + :param input_path: Path of unprocessed file + :param output_path: Destination path of unprocessed file + :param error: Recoding exception + :return: None + """ self.copy_unprocessed(input_path, output_path) self.__errors += 1 if not self.__params.hide_errors: self.__printer.error(f"File {input_path.name} can't be processed! Error: {error}") def copy_unprocessed(self, input_path: Path, output_path: Path): + """ + Method copies an unprocessed file from the source folder to the destination folder + :param input_path: Path of unprocessed file + :param output_path: Destination path of unprocessed file + :return: None + """ if self.__params.copy_unprocessed: copyfile(input_path, output_path) self.__printer.info(f"File {input_path.name} copied to compressed folder.") def catch_duplicates(self, path: Path) -> Path: - if path.exists(): + """ + Method checks if file path exists and returns folder/filename(vncopy).ext path + if duplicate founded + :param path: Some file Path + :return: Duplicate path name with (vncopy) on end + """ + if path.is_file() and path.exists(): new_path = Path(path.stem + "(vncopy)" + path.suffix) self.__duplicates.append(new_path) return new_path return path def print_duplicates(self): + """ + Method prints message about all duplicates generated during recode process + :return: None + """ for filename in self.__duplicates: self.__printer.warning( f'Duplicate file has been found! Check manually this files - "{filename.name}", ' @@ -89,6 +119,12 @@ class Utils: ) def out_rename(self, out_path: Path, target: str): + """ + Method removes md5 hash from file name and changes file extension in dependence of mimic mode + :param out_path: Recoded file Path + :param target: Target filename + :return: None + """ if not self.__params.mimic_mode: dest_name = self.catch_duplicates(Path(out_path.parent, target)) os.rename(out_path, dest_name)