Compare commits

..

No commits in common. "0a9114ff64dbf9c88dbe4661d422985918027b0c" and "9612d30d06e344b2ca7b58794335f0b2d10ae72c" have entirely different histories.

7 changed files with 81 additions and 116 deletions

View file

@ -36,5 +36,5 @@ Python utility uses ffmpeg to compress Visual Novel Resources
* [x] Recreate whole game directory with compressed files * [x] Recreate whole game directory with compressed files
* [x] Cross-platform (Easy Windows usage and binaries, macOS binaries) * [x] Cross-platform (Easy Windows usage and binaries, macOS binaries)
* [x] Use ffmpeg python bindings instead of cli commands * [x] Use ffmpeg python bindings instead of cli commands
* [x] Multithread * [ ] Reorganize code
* [ ] Reorganize code * [ ] Multithread

View file

@ -4,7 +4,6 @@ ForceCompress = false
MimicMode = true MimicMode = true
HideErrors = true HideErrors = true
WebpRGBA = true WebpRGBA = true
Workers = 16
[AUDIO] [AUDIO]
Extension = "opus" Extension = "opus"

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from concurrent.futures import ThreadPoolExecutor, as_completed from modules import configloader
from modules.configloader import config
from modules import compressor from modules import compressor
from modules import printer from modules import printer
from modules import utils from modules import utils
@ -11,49 +10,70 @@ import sys
import os import os
def get_args(): def get_file_type(filename):
try: audio_ext = ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.opus', '.raw', '.wav', '.wma']
if sys.argv[1][len(sys.argv[1])-1] == "/": image_ext = ['.apng', '.avif', '.bmp', '.tga', '.tiff', '.dds', '.svg', '.webp', '.jpg', '.jpeg', '.png']
path = sys.argv[1][:len(sys.argv[1])-1] video_ext = ['.3gp' '.amv', '.avi', '.gif', '.m2t', '.m4v', '.mkv', '.mov', '.mp4', '.m4v', '.mpeg', '.mpv',
else: '.webm', '.ogv']
path = sys.argv[1]
return path
except IndexError:
print(utils.help_message())
exit()
if os.path.splitext(filename)[1] in audio_ext:
def compress_worker(folder, file, target_folder, req_folder): return "audio"
if os.path.isfile(f'{folder}/{file}'): elif os.path.splitext(filename)[1] in image_ext:
compressor.compress_file(folder, file, target_folder, req_folder) return "image"
elif os.path.splitext(filename)[1] in video_ext:
return "video"
else:
return "unknown"
if __name__ == "__main__": if __name__ == "__main__":
start_time = datetime.now() start_time = datetime.now()
printer.win_ascii_esc() printer.win_ascii_esc()
req_folder = os.path.abspath(get_args()) try:
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()
printer.bar_init(req_folder) orig_folder = os.path.abspath(arg_path)
printer.orig_folder = os.path.abspath(arg_path)
if os.path.exists(f"{req_folder}_compressed"): printer.bar_init(orig_folder)
shutil.rmtree(f"{req_folder}_compressed")
if os.path.exists(f"{orig_folder}_compressed"):
shutil.rmtree(f"{orig_folder}_compressed")
printer.info("Creating folders...") printer.info("Creating folders...")
for folder, folders, files in os.walk(req_folder): for folder, folders, files in os.walk(orig_folder):
if not os.path.exists(folder.replace(req_folder, f"{req_folder}_compressed")): if not os.path.exists(folder.replace(orig_folder, f"{orig_folder}_compressed")):
os.mkdir(folder.replace(req_folder, f"{req_folder}_compressed")) os.mkdir(folder.replace(orig_folder, f"{orig_folder}_compressed"))
printer.info(f"Compressing \"{folder.replace(req_folder, req_folder.split('/').pop())}\" folder...") printer.info(f"Compressing \"{folder.replace(orig_folder, orig_folder.split('/').pop())}\" folder...")
target_folder = folder.replace(req_folder, f"{req_folder}_compressed") target_folder = folder.replace(orig_folder, f"{orig_folder}_compressed")
for file in os.listdir(folder):
if os.path.isfile(f'{folder}/{file}'):
match get_file_type(file):
case "audio":
comp_file = compressor.compress_audio(folder, file, target_folder,
configloader.config['AUDIO']['Extension'])
case "image":
comp_file = compressor.compress_image(folder, file, target_folder,
configloader.config['IMAGE']['Extension'])
case "video":
comp_file = compressor.compress_video(folder, file, target_folder,
configloader.config['VIDEO']['Extension'])
case "unknown":
comp_file = compressor.compress(folder, file, target_folder)
with ThreadPoolExecutor(max_workers=config["FFMPEG"]["Workers"]) as executor: if configloader.config['FFMPEG']['MimicMode']:
futures = [ try:
executor.submit(compress_worker, folder, file, target_folder, req_folder) os.rename(comp_file, f'{folder}/{file}'.replace(orig_folder, f"{orig_folder}_compressed"))
for file in files except FileNotFoundError:
] pass
for future in as_completed(futures):
future.result()
utils.get_compression_status(req_folder) utils.get_compression_status(orig_folder)
utils.sys_pause() utils.sys_pause()
print(f"Time taken: {datetime.now() - start_time}") print(f"Time taken: {datetime.now() - start_time}")

View file

@ -7,22 +7,6 @@ from ffmpeg import FFmpeg, FFmpegError
import os import os
def get_file_type(filename):
audio_ext = ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.opus', '.raw', '.wav', '.wma']
image_ext = ['.apng', '.avif', '.bmp', '.tga', '.tiff', '.dds', '.svg', '.webp', '.jpg', '.jpeg', '.png']
video_ext = ['.3gp' '.amv', '.avi', '.m2t', '.m4v', '.mkv', '.mov', '.mp4', '.m4v', '.mpeg', '.mpv',
'.webm', '.ogv']
if os.path.splitext(filename)[1] in audio_ext:
return "audio"
elif os.path.splitext(filename)[1] in image_ext:
return "image"
elif os.path.splitext(filename)[1] in video_ext:
return "video"
else:
return "unknown"
def has_transparency(img): def has_transparency(img):
if img.info.get("transparency", None) is not None: if img.info.get("transparency", None) is not None:
return True return True
@ -41,12 +25,13 @@ def has_transparency(img):
def compress_audio(folder, file, target_folder, extension): def compress_audio(folder, file, target_folder, extension):
bitrate = configloader.config['AUDIO']['BitRate'] bitrate = configloader.config['AUDIO']['BitRate']
printer.files(file, os.path.splitext(file)[0], extension, f"{bitrate}")
try: try:
(FFmpeg() (FFmpeg()
.input(f'{folder}/{file}') .input(f'{folder}/{file}')
.option("hide_banner")
.output(utils.check_duplicates(f'{target_folder}/{os.path.splitext(file)[0]}.{extension}'), .output(utils.check_duplicates(f'{target_folder}/{os.path.splitext(file)[0]}.{extension}'),
{"b:a": bitrate, "loglevel": "error"}) {"b:a": bitrate})
.execute() .execute()
) )
except FFmpegError as e: except FFmpegError as e:
@ -54,7 +39,6 @@ def compress_audio(folder, file, target_folder, extension):
utils.errors_count += 1 utils.errors_count += 1
if not configloader.config['FFMPEG']['HideErrors']: if not configloader.config['FFMPEG']['HideErrors']:
printer.error(f"File {file} can't be processed! Error: {e}") printer.error(f"File {file} can't be processed! Error: {e}")
printer.files(file, os.path.splitext(file)[0], extension, f"{bitrate}")
return f'{target_folder}/{os.path.splitext(file)[0]}.{extension}' return f'{target_folder}/{os.path.splitext(file)[0]}.{extension}'
@ -63,16 +47,14 @@ def compress_video(folder, file, target_folder, extension):
codec = configloader.config['VIDEO']['Codec'] codec = configloader.config['VIDEO']['Codec']
crf = configloader.config['VIDEO']['CRF'] crf = configloader.config['VIDEO']['CRF']
printer.files(file, os.path.splitext(file)[0], extension, codec)
try: try:
(FFmpeg() (FFmpeg()
.input(f'{folder}/{file}') .input(f'{folder}/{file}')
.option("hide_banner")
.option("hwaccel", "auto")
.output(utils.check_duplicates(f'{target_folder}/{os.path.splitext(file)[0]}.{extension}'), .output(utils.check_duplicates(f'{target_folder}/{os.path.splitext(file)[0]}.{extension}'),
{"codec:v": codec, "v:b": 0, "loglevel": "error"}, crf=crf) {"codec:v": codec, "v:b": 0}, crf=crf)
.execute() .execute()
) )
printer.files(file, os.path.splitext(file)[0], extension, codec)
except FFmpegError as e: except FFmpegError as e:
utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}') utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}')
utils.errors_count += 1 utils.errors_count += 1
@ -85,6 +67,7 @@ def compress_video(folder, file, target_folder, extension):
def compress_image(folder, file, target_folder, extension): def compress_image(folder, file, target_folder, extension):
quality = configloader.config['IMAGE']['Quality'] quality = configloader.config['IMAGE']['Quality']
printer.files(file, os.path.splitext(file)[0], extension, f"{quality}%")
try: try:
image = Image.open(f'{folder}/{file}') image = Image.open(f'{folder}/{file}')
@ -94,9 +77,6 @@ def compress_image(folder, file, target_folder, extension):
printer.warning(f"{file} has transparency. Changing to fallback...") printer.warning(f"{file} has transparency. Changing to fallback...")
extension = configloader.config['IMAGE']['FallBackExtension'] extension = configloader.config['IMAGE']['FallBackExtension']
if has_transparency(image):
image.convert('RGBA')
res_downscale = configloader.config['IMAGE']['ResDownScale'] res_downscale = configloader.config['IMAGE']['ResDownScale']
if res_downscale != 1: if res_downscale != 1:
width, height = image.size width, height = image.size
@ -108,7 +88,6 @@ def compress_image(folder, file, target_folder, extension):
lossless=configloader.config['IMAGE']['Lossless'], lossless=configloader.config['IMAGE']['Lossless'],
quality=quality, quality=quality,
minimize_size=True) minimize_size=True)
printer.files(file, os.path.splitext(file)[0], extension, f"{quality}%")
except Exception as e: except Exception as e:
utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}') utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}')
utils.errors_count += 1 utils.errors_count += 1
@ -134,27 +113,3 @@ def compress(folder, file, target_folder):
else: else:
utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}') utils.add_unprocessed_file(f'{folder}/{file}', f'{target_folder}/{file}')
return f'{target_folder}/{file}' return f'{target_folder}/{file}'
def compress_file(_dir, filename, target_dir, source):
match get_file_type(filename):
case "audio":
comp_file = compress_audio(_dir, filename, target_dir,
configloader.config['AUDIO']['Extension'])
case "image":
comp_file = compress_image(_dir, filename, target_dir,
configloader.config['IMAGE']['Extension'])
case "video":
comp_file = compress_video(_dir, filename, target_dir,
configloader.config['VIDEO']['Extension'])
case "unknown":
comp_file = compress(_dir, filename, target_dir)
if configloader.config['FFMPEG']['MimicMode']:
try:
os.rename(comp_file, f'{_dir}/{filename}'.replace(source, f"{source}_compressed"))
except FileNotFoundError:
pass
printer.bar.update()
printer.bar.next()

View file

@ -4,46 +4,42 @@ import sys
import os import os
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%%)')
bar.update()
def bar_print(string):
print(string)
bar.update()
# Fill whole string with spaces for cleaning progress bar # Fill whole string with spaces for cleaning progress bar
def clean_str(string): def clean_str(string):
return string + " " * (os.get_terminal_size().columns - len(string)) return string + " " * (os.get_terminal_size().columns - len(string))
def info(string): def info(string):
bar_print(clean_str(f"\r\033[100m- {string}\033[49m")) print(clean_str(f"\r\033[100mI {string}\033[49m"))
def warning(string): def warning(string):
bar_print(clean_str(f"\r\033[93m!\033[0m {string}\033[49m")) print(clean_str(f"\r\033[93mW\033[0m {string}\033[49m"))
def error(string): def error(string):
bar_print(clean_str(f"\r\033[31m\u2715\033[0m {string}\033[49m")) print(clean_str(f"\r\033[31mE\033[0m {string}\033[49m"))
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%%)')
def files(source, dest, dest_ext, comment): def files(source, dest, dest_ext, comment):
source_ext = os.path.splitext(source)[1] source_ext = os.path.splitext(source)[1]
source_name = os.path.splitext(source)[0] source_name = os.path.splitext(source)[0]
bar_print(clean_str(f"\r\033[0;32m\u2713\033[0m \033[0;37m{source_name}\033[0m{source_ext}\033[0;37m -> {dest}\033[0m.{dest_ext}\033[0;37m ({comment})\033[0m")) print(clean_str(f"\r* \033[0;37m{source_name}\033[0m{source_ext}\033[0;37m -> {dest}\033[0m.{dest_ext}\033[0;37m ({comment})\033[0m"))
bar.next()
def unknown_file(file): def unknown_file(file):
bar_print(clean_str(f"\r* \033[0;33m{file}\033[0m (File will be force compressed via ffmpeg)")) print(clean_str(f"\r* \033[0;33m{file}\033[0m (File will be force compressed via ffmpeg)"))
bar.next()
def win_ascii_esc(): def win_ascii_esc():

View file

@ -25,7 +25,7 @@ def get_compression(orig, comp):
orig = get_dir_size(orig, processed_files) orig = get_dir_size(orig, processed_files)
comp = get_dir_size(comp, processed_files) comp = get_dir_size(comp, processed_files)
print(f"\nResult: {orig/1024/1024:.2f}MB -> {comp/1024/1024:.2f}MB ({(comp - orig)/1024/1024:.2f}MB)") print(f"Result: {orig/1024/1024:.2f}MB -> {comp/1024/1024:.2f}MB Δ {(orig - comp)/1024/1024:.2f}MB")
except ZeroDivisionError: except ZeroDivisionError:
printer.warning("Nothing compressed!") printer.warning("Nothing compressed!")
@ -55,7 +55,7 @@ def get_compression_status(orig_folder):
def add_unprocessed_file(orig_folder, new_folder): def add_unprocessed_file(orig_folder, new_folder):
if configloader.config['FFMPEG']['CopyUnprocessed']: if configloader.config['FFMPEG']['CopyUnprocessed']:
filename = orig_folder.split("/").pop() filename = orig_folder.split().pop()
copyfile(orig_folder, new_folder) copyfile(orig_folder, new_folder)
printer.info(f"File {filename} copied to compressed folder.") printer.info(f"File {filename} copied to compressed folder.")

View file

@ -2,13 +2,8 @@ init 4 python:
import os import os
for asset in renpy.list_files(): for asset in renpy.list_files():
if os.path.splitext(asset)[1] != ".rpa" and not asset.count("unpack.rpy"): # Ignore .rpa and script itself if os.path.splitext(asset)[1] != ".rpa" and asset != "unpack.rpyc":
if renpy.macintosh: output = "unpack/game/" + asset
game_path = os.path.expanduser('~') + "/" + config.name # Unpack assets to home folder (on mac you cant get cwd)
output = game_path + "/game/" + asset
else:
output = "unpack/game/" + asset # Unpack assets to game folder
if not os.path.exists(os.path.dirname(output)): if not os.path.exists(os.path.dirname(output)):
os.makedirs(os.path.dirname(output)) os.makedirs(os.path.dirname(output))