├── src ├── __init__.py ├── rect.py ├── globals.py ├── download.py ├── styles.py └── thread.py ├── patreon.png ├── preview.png ├── res └── icon.ico ├── requirements.txt ├── setup.bat ├── .gitignore ├── .vscode └── launch.json ├── setup.py ├── README.md └── main.py /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezos/video-compressor/HEAD/patreon.png -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezos/video-compressor/HEAD/preview.png -------------------------------------------------------------------------------- /res/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezos/video-compressor/HEAD/res/icon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezos/video-compressor/HEAD/requirements.txt -------------------------------------------------------------------------------- /setup.bat: -------------------------------------------------------------------------------- 1 | python -m venv .venv 2 | call .venv\Scripts\activate 3 | pip install -r requirements.txt 4 | python setup.py build 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | output/ 3 | bin/ 4 | build/ 5 | dist/ 6 | __pycache__ 7 | .DS_Store 8 | ffmpeg* 9 | changes.txt 10 | TEMP 11 | settings.json -------------------------------------------------------------------------------- /src/rect.py: -------------------------------------------------------------------------------- 1 | class Rect: 2 | def __init__(self, x, y, width, height): 3 | self.x = x 4 | self.y = y 5 | self.w = width 6 | self.h = height 7 | -------------------------------------------------------------------------------- /src/globals.py: -------------------------------------------------------------------------------- 1 | VERSION = "3.1.2" 2 | TITLE = f"CVC v{VERSION}" 3 | READY_TEXT = f"Select your videos to get started." 4 | DEFAULT_SETTINGS = {"target_size": 20.0, "use_gpu": False} 5 | 6 | ffmpeg_path = "ffmpeg" 7 | ffprobe_path = "ffprobe" 8 | queue = [] 9 | completed = [] 10 | root_dir = "" 11 | bin_dir = "" 12 | output_dir = "" 13 | res_dir = "" 14 | ffmpeg_installed = False 15 | compressing = False 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug App", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/main.py", 9 | "console": "integratedTerminal", 10 | "justMyCode": true, 11 | "cwd": "${workspaceFolder}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from src import globals as g 2 | from cx_Freeze import setup, Executable 3 | 4 | # Dependencies are automatically detected, but it might need fine tuning. 5 | build_exe_options = { 6 | "packages": [ 7 | "PyQt6", 8 | "requests", 9 | "os", 10 | "sys", 11 | "subprocess", 12 | "json", 13 | "platform", 14 | "pathlib", 15 | "threading", 16 | ], 17 | "excludes": ["tkinter"], 18 | "optimize": 2, 19 | "include_files": [("res", "res")], 20 | } 21 | 22 | base = "Win32GUI" 23 | 24 | executables = [ 25 | Executable( 26 | "main.py", 27 | base=base, 28 | target_name=f"CheezosVideoCompressor_v{g.VERSION}", 29 | icon="res/icon.ico", 30 | ) 31 | ] 32 | 33 | setup( 34 | name="CheezosVideoCompressor", 35 | version=g.VERSION, 36 | description="Compress videos to any file size.", 37 | options={"build_exe": build_exe_options}, 38 | executables=executables, 39 | ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Cheezos Video Compressor 2 | 3 | A no bullshit video compressor. 4 | 5 | ## Features 6 | 7 | - Compresses multiple videos in a queue system 8 | - Target any specific output file size in MB 9 | - Supports GPU acceleration (NVIDIA, Intel QuickSync, AMD) 10 | - Automatically downloads and installs FFmpeg (Windows) 11 | - Progress tracking with detailed status updates 12 | - Supports multiple video formats (mp4, avi, mkv, mov, wmv, flv, webm, m4v) 13 | - Two-pass encoding for optimal quality 14 | - Automatic bitrate calculation 15 | - Desktop notifications on completion 16 | - Preserves audio quality 17 | - Clean and simple user interface, no bullshit! 18 | - Settings persistence between sessions 19 | - Auto-opens output folder when complete 20 | 21 | ### New version on Patreon! 22 | 23 | [![Patreon](https://github.com/cheezos/video-compressor/blob/main/patreon.png)](https://www.patreon.com/cheezos/shop/cheezos-video-compressor-616355?utm_medium=clipboard_copy&utm_source=copyLink&utm_campaign=productshare_creator&utm_content=join_link) 24 | 25 | ## Build 26 | 27 | ### Easy Way 28 | 1. Clone the repository. 29 | 2. Run setup.bat 30 | 31 | ### Hard Way 32 | 1. Open a terminal. 33 | 2. Clone the repository with `git clone https://github.com/cheezos/video-compressor.git` 34 | 3. Enter the project directory with `cd video-compressor` 35 | 4. Create a virtual environment with `python -m venv .venv` 36 | 5. Activate the virtual environment with `.\.venv\Scripts\activate` 37 | 6. Install the required packages with `pip install -r requirements.txt` 38 | 7. Build the application with `python setup.py build` 39 | 40 | ### The Result 41 | 42 | ![Preview](https://github.com/cheezos/video-compressor/blob/main/preview.png) 43 | 44 | --- 45 | 46 | Created with Python 3.12.6, PyQt6 and the latest FFmpeg. 47 | -------------------------------------------------------------------------------- /src/download.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import shutil 4 | import src.globals as g 5 | import zipfile 6 | from PyQt6.QtCore import QThread, pyqtSignal 7 | 8 | FFMPEG_DL = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" 9 | 10 | 11 | class DownloadThread(QThread): 12 | update_log = pyqtSignal(str) 13 | update_progress = pyqtSignal(int) 14 | installed = pyqtSignal() 15 | 16 | def __init__(self, parent=None): 17 | super().__init__(parent) 18 | 19 | def download_ffmpeg(self): 20 | print("Downloading FFmpeg...") 21 | bin_path = g.bin_dir 22 | file_path = os.path.join(bin_path, "ffmpeg.zip") 23 | response = requests.get(FFMPEG_DL, stream=True) 24 | 25 | if not response.ok: 26 | print(f"Download failed: {response.status_code}\n{response.text}") 27 | return 28 | 29 | print(f"Source: {FFMPEG_DL}") 30 | total_size = response.headers.get("content-length") 31 | 32 | with open(file_path, "wb") as f: 33 | if total_size is None: 34 | f.write(response.content) 35 | else: 36 | downloaded = 0 37 | total_size = int(total_size) 38 | 39 | for chunk in response.iter_content(chunk_size=4096): 40 | downloaded += len(chunk) 41 | f.write(chunk) 42 | percentage = (downloaded / total_size) * 100 43 | downloaded_mb = downloaded / (1024 * 1024) 44 | total_mb = total_size / (1024 * 1024) 45 | message = f"Downloading FFmpeg...\n{downloaded_mb:.1f} MB / {total_mb:.1f} MB" 46 | self.update_log.emit(message) 47 | self.update_progress.emit(int(percentage)) 48 | 49 | def install_ffmpeg(self): 50 | print("Installing FFmpeg...") 51 | zip_path = os.path.join(g.bin_dir, "ffmpeg.zip") 52 | 53 | # Extract files 54 | with zipfile.ZipFile(zip_path, "r") as zip_file: 55 | zip_file.extractall(g.bin_dir) 56 | os.remove(zip_path) 57 | 58 | # Get extracted paths 59 | extracted_root = os.path.join(g.bin_dir, os.listdir(g.bin_dir)[0]) 60 | extracted_bin = os.path.join(extracted_root, "bin") 61 | 62 | # Move binaries to target directory 63 | for file_name in os.listdir(extracted_bin): 64 | src = os.path.join(extracted_bin, file_name) 65 | dst = os.path.join(g.bin_dir, file_name) 66 | try: 67 | shutil.move(src, dst) 68 | except: 69 | print(f"Skipped {file_name} - file already exists") 70 | 71 | # Cleanup 72 | shutil.rmtree(extracted_root) 73 | os.remove(os.path.join(g.bin_dir, "ffplay.exe")) 74 | 75 | def run(self): 76 | self.download_ffmpeg() 77 | self.install_ffmpeg() 78 | self.installed.emit() 79 | -------------------------------------------------------------------------------- /src/styles.py: -------------------------------------------------------------------------------- 1 | # Window dimensions 2 | from src.rect import Rect 3 | 4 | 5 | WINDOW = Rect(0, 0, 250, 400) 6 | WINDOW_HALF = Rect(0, 0, WINDOW.w // 2, WINDOW.h // 2) 7 | 8 | LABEL_STYLE = """ 9 | QLabel { 10 | qproperty-alignment: AlignCenter; 11 | padding-bottom: 2px; 12 | } 13 | """ 14 | 15 | LABEL_LOG_STYLE = """ 16 | QLabel { 17 | qproperty-alignment: AlignCenter; 18 | border: 1px solid black; 19 | border-radius: 5px; 20 | margin: 1px; 21 | } 22 | """ 23 | 24 | BUTTON_DISABLED_STYLE = """ 25 | QPushButton { 26 | background-color: #666666; 27 | color: #cccccc; 28 | border: none; 29 | border-radius: 5px; 30 | } 31 | """ 32 | 33 | BUTTON_SELECT_STYLE = """ 34 | QPushButton { 35 | background-color: #039dfc; 36 | color: white; 37 | font-weight: bold; 38 | border: none; 39 | border-radius: 5px; 40 | } 41 | QPushButton:hover { 42 | background-color: #0384d5; 43 | border: 1px solid #026ba9; 44 | } 45 | QPushButton:pressed { 46 | background-color: #026ba9; 47 | padding: 1px; 48 | } 49 | """ 50 | 51 | BUTTON_COMPRESS_STYLE = """ 52 | QPushButton { 53 | background-color: #4CAF50; 54 | color: white; 55 | font-weight: bold; 56 | border: none; 57 | border-radius: 5px; 58 | } 59 | QPushButton:hover { 60 | background-color: #45a049; 61 | border: 1px solid #3d8b40; 62 | } 63 | QPushButton:pressed { 64 | background-color: #3d8b40; 65 | padding: 1px; 66 | } 67 | """ 68 | 69 | BUTTON_ABORT_STYLE = """ 70 | QPushButton { 71 | background-color: #f44336; 72 | color: white; 73 | font-weight: bold; 74 | border: none; 75 | border-radius: 5px; 76 | } 77 | QPushButton:hover { 78 | background-color: #da190b; 79 | border: 1px solid #b71c1c; 80 | } 81 | QPushButton:pressed { 82 | background-color: #b71c1c; 83 | padding: 1px; 84 | } 85 | """ 86 | 87 | PROGRESS_BAR_STYLE = """ 88 | QProgressBar { 89 | min-height: 25px; 90 | max-height: 25px; 91 | border: 1px solid black; 92 | border-radius: 5px; 93 | text-align: center; 94 | } 95 | QProgressBar::chunk { 96 | min-height: 25px; 97 | max-height: 25px; 98 | border-radius: 5px; 99 | text-align: center; 100 | background-color: #4CAF50; 101 | } 102 | """ 103 | 104 | CHECKBOX_STYLE = """ 105 | QCheckBox::indicator { 106 | width: 25px; 107 | height: 25px; 108 | } 109 | """ 110 | 111 | LINEEDIT_STYLE = """ 112 | QLineEdit { 113 | border: 1px solid black; 114 | border-radius: 5px; 115 | qproperty-alignment: AlignCenter; 116 | } 117 | QLineEdit:focus { 118 | border: 1px solid black; 119 | } 120 | QLineEdit:hover { 121 | border: 1px solid black; 122 | } 123 | """ 124 | 125 | 126 | # Gaps 127 | H_GAP = 5 # Horizontal gap 128 | V_GAP = 5 # Vertical gap 129 | 130 | # Buttons and elements 131 | SELECT_BUTTON = Rect( 132 | H_GAP, # Start with H_GAP from left 133 | V_GAP, # Start with V_GAP from top 134 | WINDOW.w - (H_GAP * 2), # Full width minus gaps on both sides 135 | 50, 136 | ) 137 | 138 | COMPRESS_BUTTON = Rect( 139 | H_GAP, 140 | SELECT_BUTTON.y + SELECT_BUTTON.h + V_GAP, 141 | (WINDOW.w - (H_GAP * 3)) // 2, # Half width minus gaps 142 | 50, 143 | ) 144 | 145 | ABORT_BUTTON = Rect( 146 | COMPRESS_BUTTON.x + COMPRESS_BUTTON.w + H_GAP, 147 | COMPRESS_BUTTON.y, 148 | COMPRESS_BUTTON.w, 149 | COMPRESS_BUTTON.h, 150 | ) 151 | 152 | FILE_SIZE_LABEL = Rect( 153 | H_GAP, 154 | COMPRESS_BUTTON.y + COMPRESS_BUTTON.h + V_GAP, 155 | (COMPRESS_BUTTON.w // 2) - (H_GAP // 2), 156 | 25, 157 | ) 158 | 159 | FILE_SIZE_ENTRY = Rect( 160 | COMPRESS_BUTTON.x + (COMPRESS_BUTTON.w // 2) + (H_GAP // 2), 161 | FILE_SIZE_LABEL.y, 162 | COMPRESS_BUTTON.w // 2, 163 | 25, 164 | ) 165 | 166 | GPU_LABEL = Rect( 167 | H_GAP, 168 | FILE_SIZE_LABEL.y + FILE_SIZE_LABEL.h + V_GAP, 169 | COMPRESS_BUTTON.w // 2, 170 | 25, 171 | ) 172 | 173 | GPU_CHECKBOX = Rect( 174 | FILE_SIZE_ENTRY.x + (FILE_SIZE_ENTRY.w // 2) - 12, 175 | GPU_LABEL.y, 176 | 25, 177 | 25, 178 | ) 179 | 180 | PROGRESS_BAR = Rect( 181 | H_GAP, 182 | WINDOW.h - V_GAP - 25, 183 | WINDOW.w - (H_GAP * 2), 184 | 25, 185 | ) 186 | 187 | LOG_AREA = Rect( 188 | H_GAP, 189 | GPU_LABEL.y + GPU_LABEL.h + V_GAP, 190 | WINDOW.w - (H_GAP * 2), 191 | PROGRESS_BAR.y - (GPU_LABEL.y + GPU_LABEL.h + V_GAP * 2), 192 | ) 193 | -------------------------------------------------------------------------------- /src/thread.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import os 4 | import src.globals as g 5 | from math import ceil, floor 6 | from PyQt6.QtCore import QThread, pyqtSignal 7 | 8 | 9 | def get_video_length(file_path): 10 | cmd = [ 11 | g.ffprobe_path, 12 | "-v", 13 | "quiet", 14 | "-show_entries", 15 | "format=duration", 16 | "-of", 17 | "json", 18 | file_path, 19 | ] 20 | 21 | output = subprocess.check_output(cmd) 22 | data = json.loads(output) 23 | 24 | if "format" in data: 25 | duration = data["format"].get("duration") 26 | return float(duration) if duration else 0 27 | 28 | return 0 29 | 30 | 31 | def get_audio_bitrate(video_path): 32 | cmd = [ 33 | g.ffprobe_path, 34 | "-v", 35 | "quiet", 36 | "-select_streams", 37 | "a:0", 38 | "-show_entries", 39 | "stream=bit_rate", 40 | "-of", 41 | "json", 42 | video_path, 43 | ] 44 | 45 | # Run ffprobe and capture output 46 | output = subprocess.check_output(cmd) 47 | data = json.loads(output) 48 | 49 | # Extract bitrate from JSON response 50 | if "streams" in data and len(data["streams"]) > 0: 51 | bitrate = data["streams"][0].get("bit_rate") 52 | return round(float(bitrate) / 1000) if bitrate else 0 53 | 54 | return 0 55 | 56 | 57 | def calculate_video_bitrate(file_path, target_size_mb): 58 | v_len = get_video_length(file_path) 59 | print(f"Video duration: {v_len} seconds") 60 | a_rate = get_audio_bitrate(file_path) 61 | print(f"Audio Bitrate: {a_rate}k") 62 | total_bitrate = (target_size_mb * 8192.0 * 0.98) / (1.048576 * v_len) - a_rate 63 | return max(1, round(total_bitrate)) 64 | 65 | 66 | class CompressionThread(QThread): 67 | update_log = pyqtSignal(str) 68 | update_progress = pyqtSignal(int) 69 | completed = pyqtSignal() 70 | 71 | def __init__(self, target_size_mb, use_gpu, parent=None): 72 | super().__init__(parent) 73 | self.target_size_mb = target_size_mb 74 | self.use_gpu = use_gpu 75 | self.process = None 76 | 77 | def detect_gpu_encoder(self): 78 | try: 79 | cmd = [g.ffmpeg_path, "-hide_banner", "-encoders"] 80 | output = subprocess.check_output(cmd, universal_newlines=True) 81 | 82 | if "h264_nvenc" in output: 83 | return "h264_nvenc" 84 | elif "h264_qsv" in output: # Intel QuickSync 85 | return "h264_qsv" 86 | elif "h264_amf" in output: # AMD 87 | return "h264_amf" 88 | else: 89 | return None 90 | 91 | except subprocess.CalledProcessError: 92 | return None 93 | 94 | def run_pass(self, file_path): 95 | video_rate = calculate_video_bitrate(file_path, self.target_size_mb) 96 | gpu_encoder = self.detect_gpu_encoder() if self.use_gpu else None 97 | file_name = os.path.basename(file_path) 98 | 99 | for i in range(2): 100 | # Calculate total progress based on queue position and current pass 101 | total_steps = len(g.queue) * 2 # Total number of passes for all videos 102 | current_step = ( 103 | len(g.completed) * 2 104 | ) + i # Completed videos * 2 passes + current pass 105 | progress_percentage = (current_step / total_steps) * 100 106 | self.update_progress.emit(int(progress_percentage)) 107 | encoder_type = ( 108 | f"GPU ({gpu_encoder})" if self.use_gpu and gpu_encoder else "CPU" 109 | ) 110 | status_msg = f""" 111 | [Compression Status] 112 | File: {file_name} 113 | Queue: {len(g.completed) + 1}/{len(g.queue)} 114 | Pass: {i + 1}/2 115 | Target Size: {self.target_size_mb}MB 116 | Bitrate: {video_rate}k 117 | Encoder: {encoder_type} 118 | """ 119 | 120 | # Rest of the existing code remains the same 121 | bitrate_str = f"{video_rate}k" 122 | file_name_without_ext, original_ext = os.path.basename(file_path).rsplit( 123 | ".", 1 124 | ) 125 | output_path = os.path.join( 126 | g.output_dir, f"{file_name_without_ext}-compressed.{original_ext}" 127 | ) 128 | print(f"New bitrate: {bitrate_str}") 129 | print(status_msg) 130 | 131 | # Base command arguments 132 | cmd_args = [ 133 | f'"{g.ffmpeg_path}"', 134 | f'-i "{file_path}"', 135 | "-y", 136 | f"-b:v {bitrate_str}", 137 | ] 138 | 139 | if self.use_gpu and gpu_encoder: 140 | print("Using GPU") 141 | cmd_args.extend([f"-c:v {gpu_encoder}"]) 142 | else: 143 | print("Using CPU") 144 | cmd_args.extend(["-c:v libx264"]) 145 | 146 | if i == 0: 147 | cmd_args.extend(["-an", "-pass 1", "-f mp4 TEMP"]) 148 | else: 149 | cmd_args.extend(["-pass 2", f'"{output_path}"']) 150 | 151 | cmd = " ".join(cmd_args) 152 | print(f"Running command: {cmd}") 153 | self.update_log.emit(status_msg) 154 | self.process = subprocess.check_call(cmd, shell=False) 155 | 156 | def run(self): 157 | g.completed = [] 158 | 159 | for file_path in g.queue: 160 | if not g.compressing: 161 | break 162 | 163 | self.run_pass(file_path) 164 | g.completed.append(file_path) 165 | 166 | msg = ( 167 | f"Compressed {len(g.completed)} video(s)!" if g.compressing else "Aborted!" 168 | ) 169 | 170 | print(msg) 171 | self.update_log.emit(msg) 172 | self.completed.emit() 173 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | import os 4 | import os 5 | import psutil 6 | import src.globals as g 7 | from notifypy import Notify 8 | from src.download import DownloadThread 9 | from src.thread import CompressionThread 10 | from PyQt6.QtWidgets import ( 11 | QApplication, 12 | QWidget, 13 | QPushButton, 14 | QFileDialog, 15 | QLabel, 16 | QLineEdit, 17 | QCheckBox, 18 | QProgressBar, 19 | ) 20 | from PyQt6.QtGui import QIcon 21 | from src.styles import * 22 | 23 | 24 | def load_settings(): 25 | try: 26 | with open(os.path.join(g.res_dir, "settings.json"), "r") as f: 27 | return json.load(f) 28 | except: 29 | return g.DEFAULT_SETTINGS 30 | 31 | 32 | def save_settings(settings): 33 | with open(os.path.join(g.res_dir, "settings.json"), "w") as f: 34 | json.dump(settings, f) 35 | 36 | 37 | def kill_ffmpeg(): 38 | for proc in psutil.process_iter(): 39 | if "ffmpeg" in proc.name(): 40 | proc.kill() 41 | 42 | 43 | def delete_bin(): 44 | print("@@@@@@@@@@@@@@@@@@@@@@ DELETING BIN @@@@@@@@@@@@@@@@@@@@@@@") 45 | for root, dirs, files in os.walk(g.bin_dir, topdown=False): 46 | for name in files: 47 | os.remove(os.path.join(root, name)) 48 | for name in dirs: 49 | os.rmdir(os.path.join(root, name)) 50 | 51 | 52 | class Window(QWidget): 53 | def __init__(self) -> None: 54 | super().__init__() 55 | self.verify_directories() 56 | self.settings = load_settings() 57 | self.setFixedSize(WINDOW.w, WINDOW.h) 58 | self.setWindowTitle(g.TITLE) 59 | icon_path = os.path.join(g.res_dir, "icon.ico") 60 | self.setWindowIcon(QIcon(icon_path)) 61 | 62 | # Select Button 63 | self.button_select = QPushButton("Select Videos", self) 64 | self.button_select.resize(SELECT_BUTTON.w, SELECT_BUTTON.h) 65 | self.button_select.move(SELECT_BUTTON.x, SELECT_BUTTON.y) 66 | self.button_select.clicked.connect(self.select_videos) 67 | self.button_select.setEnabled(False) 68 | 69 | # Compress Button 70 | self.button_compress = QPushButton("Compress", self) 71 | self.button_compress.resize(COMPRESS_BUTTON.w, COMPRESS_BUTTON.h) 72 | self.button_compress.move(COMPRESS_BUTTON.x, COMPRESS_BUTTON.y) 73 | self.button_compress.clicked.connect(self.compress_videos) 74 | self.button_compress.setEnabled(False) 75 | 76 | # Abort Button 77 | self.button_abort = QPushButton("Abort", self) 78 | self.button_abort.resize(ABORT_BUTTON.w, ABORT_BUTTON.h) 79 | self.button_abort.move(ABORT_BUTTON.x, ABORT_BUTTON.y) 80 | self.button_abort.clicked.connect(self.abort_compression) 81 | self.button_abort.setEnabled(False) 82 | 83 | # File Size Label 84 | self.label_size = QLabel("Size (MB)", self) 85 | self.label_size.resize(FILE_SIZE_LABEL.w, FILE_SIZE_LABEL.h) 86 | self.label_size.move(FILE_SIZE_LABEL.x, FILE_SIZE_LABEL.y) 87 | 88 | # File Size Entry 89 | self.edit_size = QLineEdit(str(self.settings["target_size"]), self) 90 | self.edit_size.resize(FILE_SIZE_ENTRY.w, FILE_SIZE_ENTRY.h) 91 | self.edit_size.move(FILE_SIZE_ENTRY.x, FILE_SIZE_ENTRY.y) 92 | self.edit_size.setEnabled(True) 93 | 94 | # GPU Label 95 | self.label_gpu = QLabel("Use GPU", self) 96 | self.label_gpu.resize(GPU_LABEL.w, GPU_LABEL.h) 97 | self.label_gpu.move(GPU_LABEL.x, GPU_LABEL.y) 98 | 99 | # GPU Checkbox 100 | self.checkbox_gpu = QCheckBox(self) 101 | self.checkbox_gpu.resize(GPU_CHECKBOX.w, GPU_CHECKBOX.h) 102 | self.checkbox_gpu.move(GPU_CHECKBOX.x, GPU_CHECKBOX.y) 103 | self.checkbox_gpu.setChecked(self.settings["use_gpu"]) 104 | 105 | # Log Label 106 | self.label_log = QLabel(g.READY_TEXT, self) 107 | self.label_log.setEnabled(True) 108 | self.label_log.resize(LOG_AREA.w, LOG_AREA.h) 109 | self.label_log.move(LOG_AREA.x, LOG_AREA.y) 110 | self.label_log.setWordWrap(True) 111 | 112 | # Progress Bar 113 | self.progress_bar = QProgressBar(self) 114 | self.progress_bar.resize(PROGRESS_BAR.w, PROGRESS_BAR.h) 115 | self.progress_bar.move(PROGRESS_BAR.x, PROGRESS_BAR.y) 116 | self.progress_bar.setRange(0, 100) 117 | 118 | self.download_thread = None 119 | self.compress_thread = None 120 | 121 | self.button_select.setStyleSheet(BUTTON_DISABLED_STYLE) 122 | self.button_compress.setStyleSheet(BUTTON_DISABLED_STYLE) 123 | self.button_abort.setStyleSheet(BUTTON_DISABLED_STYLE) 124 | self.label_size.setStyleSheet(LABEL_STYLE) 125 | self.edit_size.setStyleSheet(LINEEDIT_STYLE) 126 | self.label_gpu.setStyleSheet(LABEL_STYLE) 127 | self.checkbox_gpu.setStyleSheet(CHECKBOX_STYLE) 128 | self.label_log.setStyleSheet(LABEL_LOG_STYLE) 129 | self.progress_bar.setStyleSheet(PROGRESS_BAR_STYLE) 130 | 131 | self.verify_ffmpeg() 132 | 133 | def closeEvent(self, event): 134 | # Save settings when closing 135 | self.settings["target_size"] = float(self.edit_size.text()) 136 | self.settings["use_gpu"] = self.checkbox_gpu.isChecked() 137 | save_settings(self.settings) 138 | kill_ffmpeg() 139 | 140 | if os.path.exists(os.path.join(g.root_dir, "TEMP")): 141 | os.remove(os.path.join(g.root_dir, "TEMP")) 142 | 143 | event.accept() 144 | 145 | def reset(self): 146 | g.compressing = False 147 | g.queue = [] 148 | self.button_select.setEnabled(True) 149 | self.button_select.setStyleSheet(BUTTON_SELECT_STYLE) 150 | self.button_select.setFocus() 151 | self.button_compress.setEnabled(False) 152 | self.button_compress.setStyleSheet(BUTTON_DISABLED_STYLE) 153 | self.button_abort.setEnabled(False) 154 | self.button_abort.setStyleSheet(BUTTON_DISABLED_STYLE) 155 | self.edit_size.setEnabled(True) 156 | self.update_log(g.READY_TEXT) 157 | self.update_progress(0) 158 | 159 | def verify_directories(self): 160 | print("Verifying directories...") 161 | if getattr(sys, "frozen", False): 162 | # Running as compiled executable 163 | g.root_dir = os.path.dirname(sys.executable) 164 | else: 165 | # Running as script 166 | g.root_dir = os.path.dirname(os.path.abspath(__file__)) 167 | 168 | print(f"Root: {g.root_dir}") 169 | g.bin_dir = os.path.join(g.root_dir, "bin") 170 | 171 | if not os.path.exists(g.bin_dir): 172 | os.mkdir(g.bin_dir) 173 | 174 | print(f"Bin: {g.bin_dir}") 175 | g.output_dir = os.path.join(g.root_dir, "output") 176 | 177 | if not os.path.exists(g.output_dir): 178 | os.mkdir(g.output_dir) 179 | 180 | print(f"Output: {g.output_dir}") 181 | g.res_dir = os.path.join(g.root_dir, "res") 182 | print(f"Res: {g.res_dir}") 183 | 184 | def verify_ffmpeg(self): 185 | print("Verifying FFmpeg...") 186 | FFMPEG_PATH = os.path.join(g.bin_dir, "ffmpeg.exe") 187 | print(f"FFmpeg: {FFMPEG_PATH}") 188 | FFPROBE_PATH = os.path.join(g.bin_dir, "ffprobe.exe") 189 | print(f"FFprobe: {FFPROBE_PATH}") 190 | 191 | if os.path.exists(FFMPEG_PATH) and os.path.exists(FFPROBE_PATH): 192 | g.ffmpeg_installed = True 193 | g.ffmpeg_path = FFMPEG_PATH 194 | g.ffprobe_path = FFPROBE_PATH 195 | self.reset() 196 | else: 197 | self.download_thread = DownloadThread() 198 | self.download_thread.installed.connect(self.installed) 199 | self.download_thread.update_log.connect(self.update_log) 200 | self.download_thread.update_progress.connect(self.update_progress) 201 | self.download_thread.start() 202 | 203 | def select_videos(self): 204 | file_paths, _ = QFileDialog.getOpenFileNames( 205 | self, 206 | "Select Video Files", 207 | "", 208 | "Video Files (*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.m4v);;All Files (*.*)", 209 | ) 210 | 211 | if len(file_paths) > 0: 212 | for PATH in file_paths: 213 | if PATH in g.queue: 214 | continue 215 | 216 | g.queue.append(PATH) 217 | 218 | self.button_compress.setEnabled(True) 219 | self.button_compress.setStyleSheet(BUTTON_COMPRESS_STYLE) 220 | print(f"Selected: {g.queue}") 221 | msg = f"Selected {len(g.queue)} video(s)." 222 | self.update_log(msg) 223 | 224 | def compress_videos(self): 225 | g.compressing = True 226 | self.button_select.setStyleSheet(BUTTON_DISABLED_STYLE) 227 | self.button_compress.setStyleSheet(BUTTON_DISABLED_STYLE) 228 | self.button_abort.setEnabled(True) 229 | self.button_abort.setStyleSheet(BUTTON_ABORT_STYLE) 230 | self.button_select.setEnabled(False) 231 | self.button_compress.setEnabled(False) 232 | self.edit_size.setEnabled(False) 233 | self.compress_thread = CompressionThread( 234 | float(self.edit_size.text()), self.checkbox_gpu.isChecked() 235 | ) 236 | self.compress_thread.completed.connect(self.completed) 237 | self.compress_thread.update_log.connect(self.update_log) 238 | self.compress_thread.update_progress.connect(self.update_progress) 239 | self.compress_thread.start() 240 | 241 | def abort_compression(self): 242 | kill_ffmpeg() 243 | self.completed(True) 244 | 245 | def update_log(self, text): 246 | self.label_log.setText(text) 247 | 248 | def update_progress(self, progress_percentage): 249 | self.progress_bar.setValue(progress_percentage) 250 | 251 | def installed(self): 252 | g.ffmpeg_installed = True 253 | g.ffmpeg_path = os.path.join(g.bin_dir, "ffmpeg.exe") 254 | g.ffprobe_path = os.path.join(g.bin_dir, "ffprobe.exe") 255 | self.reset() 256 | n = Notify() 257 | n.title = "FFmpeg installed!" 258 | n.message = "You can now compress your videos." 259 | n.icon = os.path.join(g.res_dir, "icon.ico") 260 | n.send() 261 | 262 | def completed(self, aborted=False): 263 | g.compressing = False 264 | self.compress_thread.terminate() 265 | self.reset() 266 | n = Notify() 267 | n.title = "Done!" if not aborted else "Aborted!" 268 | n.message = ( 269 | "Your videos are ready." if not aborted else "Your videos are cooked!" 270 | ) 271 | n.icon = os.path.join(g.res_dir, "icon.ico") 272 | n.send() 273 | 274 | if not aborted: 275 | os.startfile(g.output_dir) 276 | 277 | 278 | if __name__ == "__main__": 279 | app = QApplication(sys.argv) 280 | window = Window() 281 | window.show() 282 | sys.exit(app.exec()) 283 | --------------------------------------------------------------------------------