Compare commits

...

10 commits

13 changed files with 163 additions and 80 deletions

View file

@ -2,6 +2,7 @@
Python utility uses ffmpeg to compress Visual Novel Resources Python utility uses ffmpeg to compress Visual Novel Resources
### How to use ### How to use
* Configure utitlity in `config.toml` * Download `ffmpeg-comp.toml` and put in next to binary or in to `/etc` folder
* `python main.py {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}` * In result you get `{folder-compressed}` near with original `{folder}`

View file

@ -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"

View file

@ -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"

View file

@ -3,22 +3,31 @@
from modules import compressor from modules import compressor
from modules import printer from modules import printer
from modules import utils from modules import utils
import shutil
import sys import sys
import os import os
try: 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: except IndexError:
print(utils.help_message()) print(utils.help_message())
exit() exit()
try: orig_folder = arg_path
os.mkdir(f"{orig_folder}_compressed") printer.orig_folder = arg_path
printer.info(f"Created {orig_folder}_compressed folder")
except OSError:
printer.warning(f"{orig_folder}_compressed already exist!")
pass
printer.info("Compression started!") printer.bar_init(orig_folder)
compressor.compress(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) utils.get_compression_status(orig_folder)

View file

@ -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'] 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'] video_exts = ['.3gp' '.amv', '.avi', '.gif', '.m4v', '.mkv', '.mov', '.mp4', '.m4v', '.mpeg', '.mpv', '.webm', '.ogv']
with open("config.toml", "rb") as f: try:
config = tomllib.load(f) 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'] ffmpeg_params = config['FFMPEG']['FFmpegParams']
req_audio_ext = config['FFMPEG']['AudioExt'] req_audio_ext = config['AUDIO']['Extension']
req_image_ext = config['FFMPEG']['ImageExt'] req_image_ext = config['IMAGE']['Extension']
req_video_ext = config['FFMPEG']['VideoExt'] req_video_ext = config['VIDEO']['Extension']
def has_transparency(img): def has_transparency(img):
@ -32,42 +38,43 @@ def has_transparency(img):
return False return False
def compress(folder): def compress(root_folder, folder):
files = len(os.listdir(folder)) target_folder = folder.replace(root_folder, f"{root_folder}_compressed")
progress = 0
for file in os.listdir(folder): for file in os.listdir(folder):
if os.path.isfile(f'{folder}/{file}'): if os.path.isfile(f'{folder}/{file}'):
if os.path.splitext(file)[1] in audio_exts: if os.path.splitext(file)[1] in audio_exts:
bitrate = config['FFMPEG']['AudioBitRate'] bitrate = config['AUDIO']['BitRate']
printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_audio_ext, f"{bitrate}bit/s") printer.files(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}'") 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: elif os.path.splitext(file)[1] in image_exts:
if req_image_ext == "jpg" or req_image_ext == "jpeg": if req_image_ext == "jpg" or req_image_ext == "jpeg":
if not has_transparency(Image.open(f'{folder}/{file}')): if not has_transparency(Image.open(f'{folder}/{file}')):
jpg_comp = config['FFMPEG']['JpegComp'] jpg_comp = config['IMAGE']['JpegComp']
printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_image_ext, f"level {jpg_comp}") 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} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_image_ext}'") os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -q {jpg_comp} "
f"'{target_folder}/{os.path.splitext(file)[0]}.{req_image_ext}'")
else: else:
printer.warning(f"{file} has transparency (.jpg not support it). Skipping...") printer.warning(f"{file} has transparency (.jpg not support it). Skipping...")
else: else:
comp_level = config['FFMPEG']['CompLevel'] comp_level = config['IMAGE']['CompLevel']
printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_image_ext, f"{comp_level}%") 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} '{folder}_compressed/{os.path.splitext(file)[0]}.{req_image_ext}'") 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: elif os.path.splitext(file)[1] in video_exts:
codec = config['FFMPEG']['VideoCodec'] codec = config['VIDEO']['Codec']
printer.files(int((progress / files) * 100), file, os.path.splitext(file)[0], req_video_ext, codec) printer.files(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}'") os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} -vcodec {codec} "
f"'{target_folder}/{os.path.splitext(file)[0]}.{req_video_ext}'")
else: else:
printer.warning("File extension not recognized. This may affect the quality of the compression.") 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") printer.unknown_file(file)
os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} '{folder}_compressed/{file}'") os.system(f"ffmpeg -i '{folder}/{file}' {ffmpeg_params} '{target_folder}/{file}'")
progress += 1

View file

@ -1,21 +1,42 @@
import os 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): def info(string):
print(f"[INFO] \033[0;32m{string}\033[0m") print(clean_str(f"\r\033[0;32m[INFO]\033[0m {string}"))
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")
def warning(string): def warning(string):
print(f"\033[0;33m[WARNING] {string}\033[0m") 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()

View file

@ -1,21 +1,21 @@
from modules import printer from modules import printer
import glob
import os import os
def get_dir_size(directory, files): def get_dir_size(directory, files):
total_size = 0 total_size = 0
for f in files: for folder, folders, files in os.walk(directory):
fp = glob.glob(f'{directory}/{f}*')[0] for file in files:
if not os.path.islink(fp): if not os.path.islink(f"{folder}/{file}"):
total_size += os.path.getsize(fp) total_size += os.path.getsize(f"{folder}/{file}")
return total_size return total_size
def get_compression(orig, comp): def get_compression(orig, comp):
processed_files = [] processed_files = []
for file in os.listdir(comp): for folder, folders, files in os.walk(comp):
processed_files.append(os.path.splitext(file)[0]) for file in files:
processed_files.append(file)
try: try:
comp = 100 - int((get_dir_size(comp, processed_files) / get_dir_size(orig, processed_files)) * 100) 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 orig_folder_len = 0
comp_folder_len = 0 comp_folder_len = 0
for file in os.listdir(orig_folder): for folder, folders, file in os.walk(orig_folder):
if os.path.isfile(f'{orig_folder}/{file}'): orig_folder_len += len(file)
orig_folder_len += 1
for file in os.listdir(f'{orig_folder}_compressed'): for folder, folders, file in os.walk(f'{orig_folder}_compressed'):
if os.path.isfile(f'{orig_folder}_compressed/{file}'): comp_folder_len += len(file)
comp_folder_len += 1
if orig_folder_len == comp_folder_len: if orig_folder_len == comp_folder_len:
printer.info("Success!") printer.info("Success!")
@ -49,5 +47,5 @@ def get_compression_status(orig_folder):
def help_message(): def help_message():
text = "Usage: main.py {folder}" text = "Usage: ffmpeg-comp {folder}"
return text return text

View file

@ -0,0 +1,2 @@
Pillow==9.5.0
progress==1.6

View file

@ -3,4 +3,5 @@ Collection of tools used by administrators from VN Telegram Channel
### Tools ### Tools
* `FFMpeg-Compressor` - Python utility uses ffmpeg to compress Visual Novel Resources * `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

View file

@ -9,10 +9,15 @@ def extract_assets(file):
for content in zip_ref.namelist(): for content in zip_ref.namelist():
if content.split('/')[0] == 'assets': if content.split('/')[0] == 'assets':
zip_ref.extract(content) zip_ref.extract(content)
zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_background.png', 'assets') if os.path.splitext(file)[1] == '.apk':
zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets') try:
os.rename('assets/res/mipmap-xxxhdpi-v4/icon_background.png', 'assets/android-icon_background.png') zip_ref.extract('res/mipmap-xxxhdpi-v4/icon_background.png', 'assets')
os.rename('assets/res/mipmap-xxxhdpi-v4/icon_foreground.png', 'assets/android-icon_foreground.png') 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): def rename_files(directory):
@ -38,7 +43,8 @@ def rename_dirs(directory):
if __name__ == '__main__': if __name__ == '__main__':
for filename in os.listdir(os.getcwd()): 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='') print(f'[INFO] Extracting assets from {filename}... ', end='')
extract_assets(filename) extract_assets(filename)
print('Done') print('Done')
@ -47,9 +53,15 @@ if __name__ == '__main__':
rename_dirs('assets') rename_dirs('assets')
print('Done') print('Done')
print('[INFO] Removing unneeded files... ', end='') print('[INFO] Removing unneeded files... ', end='')
shutil.rmtree('assets/renpy') try:
shutil.rmtree('assets/res') shutil.rmtree('assets/renpy')
except FileNotFoundError:
renpy_warn = 1
if os.path.splitext(filename)[1] == '.apk':
shutil.rmtree('assets/res')
print('Done') print('Done')
if renpy_warn:
print("[WARN] File does not contain renpy folder!")
print('[INFO] Renaming directory... ', end='') print('[INFO] Renaming directory... ', end='')
os.rename('assets', f'{os.path.splitext(filename)[0]}') os.rename('assets', f'{os.path.splitext(filename)[0]}')
print('Done') print('Done')

8
RenPy-Unpacker/README.md Normal file
View file

@ -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!

12
RenPy-Unpacker/unpack.rpy Normal file
View file

@ -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()

6
build.sh Executable file
View file

@ -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