diff --git a/FFMpeg-Compressor/README.md b/FFMpeg-Compressor/README.md index 2704f0b..f30ceed 100644 --- a/FFMpeg-Compressor/README.md +++ b/FFMpeg-Compressor/README.md @@ -2,6 +2,7 @@ Python utility uses ffmpeg to compress Visual Novel Resources ### How to use -* Configure utitlity in `config.toml` -* `python main.py {folder}` +* Download `ffmpeg-comp.toml` and put in next to binary or in to `/etc` folder +* Change the configuration of the utility in `ffmpeg-comp.toml` for yourself +* `ffmpeg-comp {folder}` * In result you get `{folder-compressed}` near with original `{folder}` diff --git a/FFMpeg-Compressor/config.toml b/FFMpeg-Compressor/config.toml deleted file mode 100644 index 2600227..0000000 --- a/FFMpeg-Compressor/config.toml +++ /dev/null @@ -1,9 +0,0 @@ -[FFMPEG] -AudioBitRate = "320k" -AudioExt = "mp3" -CompLevel = 20 -ImageExt = "png" -JpegComp = 3 -FFmpegParams = "-hide_banner -loglevel error" -VideoCodec = "libvpx-vp9" -VideoExt = "webm" \ No newline at end of file diff --git a/FFMpeg-Compressor/ffmpeg-comp.toml b/FFMpeg-Compressor/ffmpeg-comp.toml new file mode 100644 index 0000000..9f117bb --- /dev/null +++ b/FFMpeg-Compressor/ffmpeg-comp.toml @@ -0,0 +1,15 @@ +[FFMPEG] +FFmpegParams = "-hide_banner -loglevel error" + +[AUDIO] +Extension = "mp3" +BitRate = "320k" + +[IMAGE] +Extension = "jpg" +CompLevel = 100 +JpegComp = 3 + +[VIDEO] +Extension = "webm" +Codec = "libvpx-vp9" diff --git a/FFMpeg-Compressor/main.py b/FFMpeg-Compressor/main.py index c789839..3a14022 100755 --- a/FFMpeg-Compressor/main.py +++ b/FFMpeg-Compressor/main.py @@ -3,22 +3,31 @@ from modules import compressor from modules import printer from modules import utils +import shutil import sys import os try: - orig_folder = sys.argv[1] + if sys.argv[1][len(sys.argv[1])-1] == "/": + arg_path = sys.argv[1][:len(sys.argv[1])-1] + else: + arg_path = sys.argv[1] except IndexError: print(utils.help_message()) exit() -try: - os.mkdir(f"{orig_folder}_compressed") - printer.info(f"Created {orig_folder}_compressed folder") -except OSError: - printer.warning(f"{orig_folder}_compressed already exist!") - pass +orig_folder = arg_path +printer.orig_folder = arg_path -printer.info("Compression started!") -compressor.compress(orig_folder) +printer.bar_init(orig_folder) + +if os.path.exists(f"{orig_folder}_compressed"): + shutil.rmtree(f"{orig_folder}_compressed") +printer.info("Creating folders...") +for folder, folders, files in os.walk(orig_folder): + if not os.path.exists(folder.replace(orig_folder, f"{orig_folder}_compressed")): + os.mkdir(folder.replace(orig_folder, f"{orig_folder}_compressed")) + + printer.info(f"Compressing \"{folder.replace(orig_folder, orig_folder.split('/').pop())}\" folder...") + compressor.compress(orig_folder, folder) utils.get_compression_status(orig_folder) diff --git a/FFMpeg-Compressor/modules/compressor.py b/FFMpeg-Compressor/modules/compressor.py index 0e83fee..5f3d959 100644 --- a/FFMpeg-Compressor/modules/compressor.py +++ b/FFMpeg-Compressor/modules/compressor.py @@ -7,13 +7,19 @@ audio_exts = ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.opus', '.raw', '.wav', image_exts = ['.apng', '.avif', '.jfif', '.pjpeg', '.pjp', '.svg', '.webp', '.jpg', '.jpeg', '.png', '.raw'] video_exts = ['.3gp' '.amv', '.avi', '.gif', '.m4v', '.mkv', '.mov', '.mp4', '.m4v', '.mpeg', '.mpv', '.webm', '.ogv'] -with open("config.toml", "rb") as f: - config = tomllib.load(f) +try: + config = tomllib.load(open("ffmpeg-comp.toml", "rb")) +except FileNotFoundError: + try: + config = tomllib.load(open("/etc/ffmpeg-comp.toml", "rb")) + except FileNotFoundError: + printer.error("Config file not found. Please put it next to binary or in to /etc folder.") + exit() ffmpeg_params = config['FFMPEG']['FFmpegParams'] -req_audio_ext = config['FFMPEG']['AudioExt'] -req_image_ext = config['FFMPEG']['ImageExt'] -req_video_ext = config['FFMPEG']['VideoExt'] +req_audio_ext = config['AUDIO']['Extension'] +req_image_ext = config['IMAGE']['Extension'] +req_video_ext = config['VIDEO']['Extension'] def has_transparency(img): @@ -32,42 +38,43 @@ def has_transparency(img): return False -def compress(folder): - files = len(os.listdir(folder)) - progress = 0 +def compress(root_folder, folder): + target_folder = folder.replace(root_folder, f"{root_folder}_compressed") for file in os.listdir(folder): if os.path.isfile(f'{folder}/{file}'): if os.path.splitext(file)[1] in audio_exts: - bitrate = config['FFMPEG']['AudioBitRate'] - printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_audio_ext, f"{bitrate}bit/s") - os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_audio_ext}'") + bitrate = config['AUDIO']['BitRate'] + printer.files(file, os.path.splitext(file)[0], req_audio_ext, f"{bitrate}bit/s") + os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} " + f"'{target_folder}/{os.path.splitext(file)[0]}.{req_audio_ext}'") elif os.path.splitext(file)[1] in image_exts: if req_image_ext == "jpg" or req_image_ext == "jpeg": if not has_transparency(Image.open(f'{folder}/{file}')): - jpg_comp = config['FFMPEG']['JpegComp'] - printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_image_ext, f"level {jpg_comp}") - os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -q {jpg_comp} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_image_ext}'") + jpg_comp = config['IMAGE']['JpegComp'] + printer.files(file, os.path.splitext(file)[0], req_image_ext, f"level {jpg_comp}") + os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -q {jpg_comp} " + f"'{target_folder}/{os.path.splitext(file)[0]}.{req_image_ext}'") else: printer.warning(f"{file} has transparency (.jpg not support it). Skipping...") else: - comp_level = config['FFMPEG']['CompLevel'] - printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_image_ext, f"{comp_level}%") - os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -compression_level {comp_level} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_image_ext}'") + comp_level = config['IMAGE']['CompLevel'] + printer.files(file, os.path.splitext(file)[0], req_image_ext, f"{comp_level}%") + os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -compression_level {comp_level} " + f"'{target_folder}/{os.path.splitext(file)[0]}.{req_image_ext}'") elif os.path.splitext(file)[1] in video_exts: - codec = config['FFMPEG']['VideoCodec'] - printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_video_ext, codec) - os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -vcodec {codec} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_video_ext}'") + codec = config['VIDEO']['Codec'] + printer.files(file, os.path.splitext(file)[0], req_video_ext, codec) + os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -vcodec {codec} " + f"'{target_folder}/{os.path.splitext(file)[0]}.{req_video_ext}'") else: printer.warning("File extension not recognized. This may affect the quality of the compression.") - print(f"\r[{int((progress/files) * 100)}%] \033[0;33m{file}\033[0m") - os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} '{folder}_compressed/{file}'") - - progress += 1 + printer.unknown_file(file) + os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} '{target_folder}/{file}'") diff --git a/FFMpeg-Compressor/modules/printer.py b/FFMpeg-Compressor/modules/printer.py index 209f813..f735b01 100644 --- a/FFMpeg-Compressor/modules/printer.py +++ b/FFMpeg-Compressor/modules/printer.py @@ -1,21 +1,42 @@ import os +from progress.bar import IncrementalBar + + +# Fill whole string with spaces for cleaning progress bar +def clean_str(string): + return string + " " * (os.get_terminal_size().columns - len(string)) def info(string): - print(f"[INFO] \033[0;32m{string}\033[0m") - - -def files(progress, source, dest, dest_ext, comment): - source_ext = os.path.splitext(source)[1] - source_name= os.path.splitext(source)[0] - - if progress < 10: - progress = f" {progress}" - elif progress < 100: - progress = f" {progress}" - - print(f"[{progress}%] \033[0;32m{source_name}\033[0m{source_ext}\033[0;32m -> {dest}\033[0m.{dest_ext}\033[0;32m ({comment})\033[0m") + print(clean_str(f"\r\033[0;32m[INFO]\033[0m {string}")) def warning(string): - print(f"\033[0;33m[WARNING] {string}\033[0m") \ No newline at end of file + print(clean_str(f"\r\033[0;33m[WARNING]\033[0m {string}")) + + +def error(string): + print(clean_str(f"\r\033[0;31m[ERROR]\033[0m {string}")) + + +def bar_init(folder): + file_count = 0 + for folder, folders, file in os.walk(folder): + file_count += len(file) + global bar + bar = IncrementalBar('Compressing', max=file_count, suffix='[%(index)d/%(max)d] (%(percent).1f%%) - ETA: %(eta)ds') + + +def files(source, dest, dest_ext, comment): + + source_ext = os.path.splitext(source)[1] + source_name = os.path.splitext(source)[0] + + print(clean_str(f"\r[COMP] \033[0;32m{source_name}\033[0m{source_ext}\033[0;32m -> {dest}\033[0m.{dest_ext}\033[0;32m ({comment})\033[0m")) + bar.next() + + +def unknown_file(file): + + print(clean_str(f"\r[COMP] \033[0;33m{file}\033[0m")) + bar.next() diff --git a/FFMpeg-Compressor/modules/utils.py b/FFMpeg-Compressor/modules/utils.py index 8cc41e2..66f3b80 100644 --- a/FFMpeg-Compressor/modules/utils.py +++ b/FFMpeg-Compressor/modules/utils.py @@ -1,21 +1,21 @@ from modules import printer -import glob import os def get_dir_size(directory, files): total_size = 0 - for f in files: - fp = glob.glob(f'{directory}/{f}*')[0] - if not os.path.islink(fp): - total_size += os.path.getsize(fp) + for folder, folders, files in os.walk(directory): + for file in files: + if not os.path.islink(f"{folder}/{file}"): + total_size += os.path.getsize(f"{folder}/{file}") return total_size def get_compression(orig, comp): processed_files = [] - for file in os.listdir(comp): - processed_files.append(os.path.splitext(file)[0]) + for folder, folders, files in os.walk(comp): + for file in files: + processed_files.append(file) try: comp = 100 - int((get_dir_size(comp, processed_files) / get_dir_size(orig, processed_files)) * 100) @@ -32,13 +32,11 @@ def get_compression_status(orig_folder): orig_folder_len = 0 comp_folder_len = 0 - for file in os.listdir(orig_folder): - if os.path.isfile(f'{orig_folder}/{file}'): - orig_folder_len += 1 + for folder, folders, file in os.walk(orig_folder): + orig_folder_len += len(file) - for file in os.listdir(f'{orig_folder}_compressed'): - if os.path.isfile(f'{orig_folder}_compressed/{file}'): - comp_folder_len += 1 + for folder, folders, file in os.walk(f'{orig_folder}_compressed'): + comp_folder_len += len(file) if orig_folder_len == comp_folder_len: printer.info("Success!") @@ -49,5 +47,5 @@ def get_compression_status(orig_folder): def help_message(): - text = "Usage: main.py {folder}" + text = "Usage: ffmpeg-comp {folder}" return text diff --git a/FFMpeg-Compressor/requirements.txt b/FFMpeg-Compressor/requirements.txt new file mode 100644 index 0000000..290af37 --- /dev/null +++ b/FFMpeg-Compressor/requirements.txt @@ -0,0 +1,2 @@ +Pillow==9.5.0 +progress==1.6 diff --git a/README.md b/README.md index 5982f00..127caae 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,5 @@ Collection of tools used by administrators from VN Telegram Channel ### Tools * `FFMpeg-Compressor` - Python utility uses ffmpeg to compress Visual Novel Resources -* `A simple Python script` for unpacking Ren'Py based .apk files for later rebuilding in the Ren'Py SDK +* `RenPy-Android-Unpack` - Simple Python script for unpacking Ren'Py based .apk files for later rebuilding in the Ren'Py SDK +* `RenPy-Unpacker` - Simple .rpy script that will make any RenPy game unpack itself diff --git a/RenPy-Android-Unpack/unpack.py b/RenPy-Android-Unpack/unpack.py index 6d826a8..07cc074 100755 --- a/RenPy-Android-Unpack/unpack.py +++ b/RenPy-Android-Unpack/unpack.py @@ -9,10 +9,15 @@ def extract_assets(file): for content in zip_ref.namelist(): if content.split('/')[0] == 'assets': zip_ref.extract(content) - zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_background.png', 'assets') - zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets') - os.rename('assets/res/mipmap-xxxhdpi-v4/icon_background.png', 'assets/android-icon_background.png') - os.rename('assets/res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets/android-icon_foreground.png') + if os.path.splitext(file)[1] == '.apk': + try: + zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_background.png', 'assets') + zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets') + os.rename('assets/res/mipmap-xxxhdpi-v4/icon_background.png', 'assets/android-icon_background.png') + os.rename('assets/res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets/android-icon_foreground.png') + except KeyError: + zip_ref.extract('res/drawable/icon.png', 'assets') + os.rename('assets/res/drawable/icon.png', 'assets/icon.png') def rename_files(directory): @@ -38,7 +43,8 @@ def rename_dirs(directory): if __name__ == '__main__': for filename in os.listdir(os.getcwd()): - if os.path.splitext(filename)[1] == '.apk': + renpy_warn = 0 + if os.path.splitext(filename)[1] == '.apk' or os.path.splitext(filename)[1] == '.obb': print(f'[INFO] Extracting assets from {filename}... ', end='') extract_assets(filename) print('Done') @@ -47,9 +53,15 @@ if __name__ == '__main__': rename_dirs('assets') print('Done') print('[INFO] Removing unneeded files... ', end='') - shutil.rmtree('assets/renpy') - shutil.rmtree('assets/res') + try: + shutil.rmtree('assets/renpy') + except FileNotFoundError: + renpy_warn = 1 + if os.path.splitext(filename)[1] == '.apk': + shutil.rmtree('assets/res') print('Done') + if renpy_warn: + print("[WARN] File does not contain renpy folder!") print('[INFO] Renaming directory... ', end='') os.rename('assets', f'{os.path.splitext(filename)[0]}') print('Done') diff --git a/RenPy-Unpacker/README.md b/RenPy-Unpacker/README.md new file mode 100644 index 0000000..41a3c75 --- /dev/null +++ b/RenPy-Unpacker/README.md @@ -0,0 +1,8 @@ +## RenPy-Unpacker +Simple .rpy script that will make any RenPy game unpack itself + +### How to use +* Put .rpyc from releases page to game's `game` folder +* Open your game and wait until it not be launched +* Unpacked assets will be in `unpack` folder near with game's executable +* Enjoy! diff --git a/RenPy-Unpacker/unpack.rpy b/RenPy-Unpacker/unpack.rpy new file mode 100644 index 0000000..005e855 --- /dev/null +++ b/RenPy-Unpacker/unpack.rpy @@ -0,0 +1,12 @@ +init 4 python: + import os + + for asset in renpy.list_files(): + if os.path.splitext(asset)[1] != ".rpa" and asset != "unpack.rpyc": + output = "unpack/game/" + asset + if not os.path.exists(os.path.dirname(output)): + os.makedirs(os.path.dirname(output)) + + out_bytes = open(output, "wb") + out_bytes.write(renpy.file(asset).read()) + out_bytes.close() diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..3eec12d --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +mkdir output +mkdir output/bin +nuitka3 --jobs=$(nproc) --output-dir=output --follow-imports --output-filename=output/bin/ffmpeg-comp FFMpeg-Compressor/main.py +cp FFMpeg-Compressor/ffmpeg-comp.toml output/bin/ +nuitka3 --jobs=$(nproc) --output-dir=output --follow-imports --output-filename=output/bin/rendroid-unpack RenPy-Android-Unpack/unpack.py \ No newline at end of file