diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..c22cb12 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: 'Bug: [short description]' +labels: bug +--- + +## Description + + + +## Steps to Reproduce +1. +2. +3. + +## Expected Behavior + + + +## Actual Behavior + + + +## Environment Details +- OS: +- Browser: +- Version: + +## Screenshots + + + +## Additional Context + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2eb245f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature Request +description: Suggest a new idea for this project +title: " +labels: feature request +--- + +## Feature Description + +A clear and concise description of what the feature is and why it is needed. + +## Problem Statement + +What is the problem that this feature would solve? Why is it important? + +## Proposed Solution + +A clear and concise description of what you want to happen. + +## Alternatives Considered + +What are other solutions you considered? Why did you choose this solution? + +## Additional Context + +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8cd0b4a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +## Description + + + + +## Type of Change + +- [ ] Bugfix +- [ ] New Feature +- [ ] Documentation Update +- [ ] Other (Please specify) + + +## Testing Checklist + +- [ ] I have tested the changes locally +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have updated the documentation accordingly + + +## Related Issues + + + + +## Screenshots + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..730bc86 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +## [1.0.0] - 2025-10-18 + +### Added +- GUI version +- CLI version with Rich library +- Batch processing +- Language selection +- Auto-detect +- Dry run mode +- Real-time progress tracking +- Cross-platform support + +### Technical Details +- Python 3.7+ support +- MKVToolNix integration +- Threading support + +### Dependencies +- Rich library +- Tkinter + +### Documentation Updates +- Improved documentation for better understanding + +### Supported Platforms +- Windows +- macOS +- Linux + +### Planned Future Enhancements +- Multi-threading support +- Presets for common configurations +- Standalone executables +- Support for additional formats +- Web interface for easier access +- Detailed logging features +- Track selection capabilities +- Codec conversion functionality \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2d83e20 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,32 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to instances of unacceptable behavior. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Enforcement Guidelines +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [your-email@example.com]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. + +## Attribution +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ca29ec9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing Guidelines + +Thank you for considering contributing to our project! We welcome contributions from everyone. Here are some guidelines to help you get started: + +## Reporting Bugs +If you encounter a bug, please open an issue in the repository and provide as much detail as possible, including steps to reproduce the bug. + +## Suggesting Features +We love new ideas! If you have a feature suggestion, please open an issue and describe your idea. + +## Code Style Guidelines +We follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) for Python code. Please ensure your code adheres to these guidelines. + +## Pull Request Process +1. Fork the repository. +2. Create a new branch for your changes. +3. Make your changes and commit them. +4. Open a pull request and describe your changes. + +## Development Setup Instructions +1. Clone the repository. +2. Install dependencies using `pip install -r requirements.txt`. +3. Run tests using `pytest`. + +## Testing Guidelines +Please ensure that you add tests for any new features or bug fixes. + +## Commit Message Conventions +Use clear and descriptive commit messages. Follow the format: `Type: Subject` where Type can be feat, fix, docs, style, refactor, perf, test. + +## Code of Conduct +Please adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) in all interactions. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46ecad5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 mlbkumar9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +- The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5c234d1..b385ca5 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,2486 @@ -# MKV Batch Remuxer (Project 13) +# MKV Batch Remuxer 🎬 -This project provides a tool to batch remux Matroska Video (MKV) files, allowing the user to select specific audio and subtitle tracks to keep while discarding the rest. It comes in two versions: a command-line interface (CLI) and a graphical user interface (GUI). +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Python](https://img.shields.io/badge/Python-3.6%2B-blue.svg)](https://www.python.org/) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)]() -## Features +## πŸ“‹ Table of Contents +- [Project Overview](#-project-overview) +- [Project Objectives](#-project-objectives) +- [Features](#-features) +- [System Architecture](#-system-architecture) +- [Folder Structure](#-folder-structure) +- [Prerequisites](#-prerequisites) +- [Installation](#-installation) +- [Usage](#-usage) +- [ISO 639-2 Language Codes Reference](#-iso-639-2-language-codes-reference) +- [Workflow and Data Flow](#-workflow-and-data-flow) +- [Practical Usage Examples](#-practical-usage-examples) +- [Troubleshooting](#-troubleshooting) +- [FAQ](#-faq) +- [Contributing](#-contributing) +- [License](#-license) +- [Support](#-support) -- **Batch Processing:** Remux multiple `.mkv` files in a single operation. -- **Language Selection:** Specify which audio and subtitle languages to keep (e.g., `eng`, `jpn`). -- **CLI and GUI:** Choose between a powerful command-line script or an easy-to-use graphical interface. -- **Flexible Options:** Includes options to skip already processed files and perform a "dry run" to see what commands will be executed without modifying any files. -- **Live Progress:** Both versions provide real-time feedback on the remuxing process. +## πŸ“– Project Overview -## Prerequisites +**MKV Batch Remuxer** is a professional, feature-rich tool designed to streamline and automate the process of remuxing MKV (Matroska Video) files. The application provides both a modern graphical user interface (GUI) and a powerful command-line interface (CLI), making it accessible to users of all technical levelsβ€”from beginners who prefer visual tools to advanced users who need scriptable automation. -Before you begin, ensure you have the following installed: +### What is Remuxing? -1. **Python 3:** Make sure Python 3 is installed and accessible from your command line. -2. **MKVToolNix:** This provides the underlying `mkvmerge` command-line tool that performs the remuxing. You can download it from the [official MKVToolNix website](https://mkvtoolnix.download/). Please ensure the installation directory is added to your system's PATH, or the script will try to find it in the default Windows location. +Remuxing is the process of changing the container format of a video file without re-encoding the actual video or audio streams. This means: +- **No quality loss**: The video and audio remain identical to the source +- **Fast processing**: Only metadata is processed, not the media streams +- **Selective track inclusion**: Choose which audio and subtitle tracks to keep +- **Reduced file size**: Remove unnecessary language tracks to save space -## Setup +### Key Benefits -1. **Clone the repository (or download the files):** - ```bash - git clone - cd Project_13 - ``` +- **Dual Interface**: Choose between intuitive GUI or efficient CLI based on your needs +- **Batch Processing**: Process multiple files simultaneously with multi-threaded support +- **Language Filtering**: Automatically detect and select desired audio/subtitle languages +- **Cross-Platform**: Works seamlessly on Windows, macOS, and Linux +- **Real-time Progress**: Live progress tracking with time estimates for all operations +- **Production Ready**: Error handling, logging, and validation for reliable operations -2. **Install the required Python packages:** - Navigate to the project directory in your terminal and run the following command to install the `rich` library, which is used by the CLI script: - ```bash - pip install -r requirements.txt - ``` +## 🎯 Project Objectives -## How to Run +1. **Simplify MKV File Management**: Provide an easy-to-use solution for managing multi-language MKV files +2. **Reduce Storage Requirements**: Enable users to remove unwanted language tracks to save disk space +3. **Maintain Quality**: Ensure lossless remuxing without re-encoding video or audio streams +4. **Enhance User Experience**: Offer both GUI and CLI interfaces to cater to different user preferences +5. **Automate Workflow**: Support batch processing to handle large video libraries efficiently +6. **Ensure Reliability**: Implement robust error handling and comprehensive logging +7. **Cross-Platform Support**: Provide consistent functionality across major operating systems +8. **Transparency**: Offer dry-run mode and detailed progress reporting for user confidence -There are two scripts located in the `REMUX Python Scripts` directory. +## ✨ Features -### 1. GUI Version (`remux_gui.py`) +### πŸ–₯️ GUI Version (REMUX_GUI.py) -This is the recommended version for most users. +#### Interface & Usability +- **Modern Tkinter-Based Interface**: Clean, professional 950x700 pixel window optimized for readability +- **Intuitive Directory Selection**: Browse buttons for easy input/output folder selection with visual feedback +- **Responsive Design**: Multi-threaded architecture prevents UI freezing during long operations +- **Queue-Based Updates**: Smooth, non-blocking progress updates using thread-safe queue communication -1. **Run the script:** - ```bash - python "REMUX Python Scripts/remux_gui.py" - ``` -2. **Using the application:** - - Click **Browse...** to select your **Input** directory (where your MKV files are) and **Output** directory. - - In the **Audio Langs** and **Subtitle Langs** fields, enter the 3-letter language codes you wish to keep, separated by commas (e.g., `eng,jpn`). - - Select the desired options (`Skip if output exists`, `Dry-run only`). - - Click **Start Remux** to begin the process. - - Progress will be displayed in the table. +#### Language Detection & Selection +- **Automatic Language Scanning**: Intelligent detection of all available audio and subtitle languages across your MKV collection +- **Visual Language Display**: Real-time display of detected languages in dedicated panels with color-coded status +- **Quick Selection Tools**: + - "Select All Audio" - Instantly select all detected audio languages + - "Select All Subtitles" - Instantly select all detected subtitle languages + - "Clear Audio" - Quick reset of audio language selection + - "Clear Subtitles" - Quick reset of subtitle language selection +- **Manual Language Input**: Direct input fields for ISO 639-2 language codes (comma-separated) +- **Language Validation**: Warns users if selected languages aren't found in the source files -### 2. Command-Line Version (`1_Improved_REMUX_Script.py`) +#### Processing Options +- **Skip Existing Files**: Smart checkbox to bypass files that already exist in the output directory (enabled by default) +- **Dry-Run Mode**: Preview what would happen without actually processing files - perfect for testing +- **Batch Processing**: Handle multiple files simultaneously with configurable threading +- **Auto-Directory Creation**: Automatically creates output directories if they don't exist -This version is for users who prefer working in the terminal. +#### Progress Monitoring +- **Real-Time Progress Table**: Comprehensive view showing: + - Filename with smart truncation for readability + - Visual progress bar for each file + - Percentage completion (0-100%) + - Elapsed time in MM:SS format + - Remaining time estimation + - Current status (Pending, Processing, OK, Skipped, Failed) +- **Completion Summary**: Final statistics showing total files processed, successful operations, and failures +- **Color-Coded Status**: Visual indicators for quick status identification + +#### Technical Features +- **MKVToolNix Integration**: Uses industry-standard `mkvmerge` tool with JSON track identification +- **Error Recovery**: Graceful handling of corrupted files, missing dependencies, or permission issues +- **Resource Efficient**: Optimized memory usage even with large file collections + +### πŸ’» CLI Version (REMUX_Script.py) + +#### User Interface +- **Rich Library Integration**: Beautiful, colorful console output with professional formatting +- **Interactive Prompts**: Step-by-step guided setup process for all configuration options +- **Terminal Compatibility**: Works with standard terminals on all platforms (Windows CMD/PowerShell, macOS/Linux Terminal) +- **Clear Visual Hierarchy**: Organized sections with headers, dividers, and color-coded messages + +#### Language Management +- **Automatic File Scanning**: Pre-scan all MKV files to detect available audio and subtitle languages +- **Intelligent Language Display**: Shows all detected languages before prompting for selection +- **Smart Selection System**: + - Enter specific languages (e.g., "eng,jpn,kor") for precise control + - Press Enter without input to select ALL detected languages + - Comma-separated input with automatic trimming and validation +- **Validation Warnings**: Real-time feedback if you select languages not present in your files + +#### Processing Features +- **Live Progress Table**: Dynamic, updating table showing: + - Serial number for each file + - Shortened filename (40 characters max) + - Animated progress bar + - Percentage completion + - Elapsed processing time + - Remaining time estimate + - Current status + - Result indicator (βœ“ for success, βœ— for failure) +- **Batch Progress Row**: Separate row showing overall batch completion with gap separator +- **Real-Time Updates**: Progress parsed directly from mkvmerge's output stream +- **Fallback Progress Calculation**: Uses file size comparison if direct progress parsing fails + +#### Logging & Debugging +- **Comprehensive Logging**: Creates `remux_log.txt` in the output directory with: + - Timestamp for each operation + - Source and destination file paths + - Selected language tracks + - Processing status and results + - Error messages and stack traces +- **Tab-Separated Format**: Structured log format for easy parsing and analysis +- **Command Storage**: Logs the exact mkvmerge commands executed for reproducibility and debugging +- **Session Summary**: Final statistics including success/failure counts + +#### Operation Modes +- **Skip Existing Files**: Configurable prompt (Y/n) to skip files that already exist in output +- **Dry-Run Mode**: Preview mode (y/N) that shows what would happen without making changes +- **Interactive Confirmation**: Prompts before starting batch operations for safety + +#### Platform Support +- **Cross-Platform Detection**: Automatically finds mkvmerge on: + - Windows (checks PATH and default installation locations) + - macOS (checks PATH and common installation directories) + - Linux (uses system PATH) +- **Path Handling**: Robust path resolution for all operating systems + +### πŸ”§ Common Functionality (Both Versions) + +#### Track Processing +- **JSON-Based Track Identification**: Uses mkvmerge's JSON output format for accurate track detection +- **Language Code Filtering**: Filters audio and subtitle tracks by ISO 639-2 language codes +- **Automatic Video Retention**: Always preserves all video tracks during remuxing +- **Selective Track Inclusion**: Include only desired audio and subtitle languages +- **Multiple Track Support**: Handles files with multiple tracks of the same type + +#### Progress & Reporting +- **GUI Mode Integration**: Uses mkvmerge's `--gui-mode` for standardized progress reporting +- **Real-Time Progress Parsing**: Extracts progress percentage from mkvmerge output +- **Time Estimation**: Calculates elapsed and remaining time based on current progress +- **MM:SS Time Format**: Human-readable time display (e.g., "05:23" for 5 minutes 23 seconds) + +#### Error Handling & Validation +- **Missing mkvmerge Detection**: Clear error message if MKVToolNix is not installed +- **Invalid Directory Handling**: Validates input/output directories before processing +- **Empty Directory Check**: Warns if no MKV files are found in the input directory +- **File Access Validation**: Checks read permissions for input files and write permissions for output +- **Corrupted File Handling**: Gracefully handles files that mkvmerge cannot process + +#### Status Management +- **Comprehensive Status Codes**: + - **Pending**: File queued for processing + - **Processing**: Currently being remuxed + - **OK**: Successfully completed + - **Skipped**: File skipped (already exists or user choice) + - **Failed**: Processing failed (with error details) +- **Undefined Language Support**: Properly handles "und" (undefined) language code +- **Exit Code Checking**: Validates mkvmerge's return code for success/failure detection + +## πŸ—οΈ System Architecture + +### High-Level Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Interface Layer β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ GUI (Tkinter) β”‚ CLI (Rich Console) β”‚ +β”‚ - Event-driven UI β”‚ - Interactive prompts β”‚ +β”‚ - Multi-threaded β”‚ - Live table display β”‚ +β”‚ - Queue-based updates β”‚ - Progress tracking β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Core Processing Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Language Detection Engine β”‚ β”‚ +β”‚ β”‚ - Scan MKV files β”‚ β”‚ +β”‚ β”‚ - Extract track metadata (JSON) β”‚ β”‚ +β”‚ β”‚ - Build language inventory β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Remuxing Engine β”‚ β”‚ +β”‚ β”‚ - Build mkvmerge commands β”‚ β”‚ +β”‚ β”‚ - Execute subprocess β”‚ β”‚ +β”‚ β”‚ - Parse progress output β”‚ β”‚ +β”‚ β”‚ - Handle errors β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Progress Tracking System β”‚ β”‚ +β”‚ β”‚ - Real-time percentage calculation β”‚ β”‚ +β”‚ β”‚ - Time estimation (elapsed/remaining) β”‚ β”‚ +β”‚ β”‚ - Status management β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ External Dependencies Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ MKVToolNix (mkvmerge) β”‚ β”‚ +β”‚ β”‚ - Track identification (--identify --json) β”‚ β”‚ +β”‚ β”‚ - File remuxing (--audio-tracks --subtitle-tracks) β”‚ β”‚ +β”‚ β”‚ - Progress reporting (--gui-mode) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Python Standard Library β”‚ β”‚ +β”‚ β”‚ - tkinter (GUI) β”‚ β”‚ +β”‚ β”‚ - subprocess (process management) β”‚ β”‚ +β”‚ β”‚ - threading (concurrency) β”‚ β”‚ +β”‚ β”‚ - queue (thread communication) β”‚ β”‚ +β”‚ β”‚ - pathlib (file system operations) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Third-Party Libraries β”‚ β”‚ +β”‚ β”‚ - rich (CLI formatting and live display) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Component Interaction Flow + +``` +[User Input] β†’ [UI Layer] β†’ [Language Detection] β†’ [Track Filtering] + ↓ ↓ ↓ ↓ +[Settings] β†’ [Validation] β†’ [Command Building] β†’ [mkvmerge Execution] + ↓ ↓ ↓ ↓ +[Progress] ← [Status Update] ← [Output Parsing] ← [Process Output] + ↓ ↓ ↓ ↓ +[UI Display] ← [Queue/Thread] ← [Result Handling] ← [File Validation] +``` + +### Threading Model (GUI) + +``` +Main Thread Worker Threads + β”‚ β”‚ + β”œβ”€ UI Rendering β”‚ + β”œβ”€ Event Handling β”‚ + β”œβ”€ Queue Processing ←─────────── + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” + β”‚ β”‚ Thread 1 β”‚ β†’ File 1 β†’ mkvmerge + β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ β”‚ Thread 2 β”‚ β†’ File 2 β†’ mkvmerge + β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + β”‚ β”‚ Thread N β”‚ β†’ File N β†’ mkvmerge + β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ β”‚ + └──────── Updates β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + (via Queue) +``` + +## πŸ“ Folder Structure + +``` +Project_13/ +β”‚ +β”œβ”€β”€ REMUX Python Scripts/ # Main application directory +β”‚ β”œβ”€β”€ REMUX_GUI.py # GUI application (Tkinter-based) +β”‚ β”‚ # - 435 lines of code +β”‚ β”‚ # - Provides graphical interface +β”‚ β”‚ # - Multi-threaded processing +β”‚ β”‚ # - Real-time progress display +β”‚ β”‚ +β”‚ └── REMUX_Script.py # CLI application (Rich-based) +β”‚ # - 566 lines of code +β”‚ # - Interactive command-line interface +β”‚ # - Live table updates +β”‚ # - Comprehensive logging +β”‚ +β”œβ”€β”€ docs/ # Documentation files +β”‚ β”œβ”€β”€ FAQ.md # Frequently asked questions +β”‚ β”œβ”€β”€ INSTALLATION.md # Detailed installation guide +β”‚ └── USAGE.md # Usage guide and examples +β”‚ +β”œβ”€β”€ .github/ # GitHub-specific files +β”‚ └── workflows/ # CI/CD workflows (if any) +β”‚ +β”œβ”€β”€ README.md # Main project documentation (this file) +β”œβ”€β”€ LICENSE # MIT License +β”œβ”€β”€ CHANGELOG.md # Version history and changes +β”œβ”€β”€ CODE_OF_CONDUCT.md # Community guidelines +β”œβ”€β”€ CONTRIBUTING.md # Contribution guidelines +β”œβ”€β”€ SECURITY.md # Security policy and reporting +β”œβ”€β”€ requirements.txt # Python dependencies (rich) +└── .gitignore # Git ignore rules + +Generated Files (Not in Repository): +──────────────────────────────────── +output_directory/ +β”œβ”€β”€ remuxed_file1.mkv # Processed MKV files +β”œβ”€β”€ remuxed_file2.mkv +β”œβ”€β”€ ... +└── remux_log.txt # CLI processing log (CLI only) + # - Tab-separated format + # - Timestamps, paths, status + # - Command history +``` + +### File Descriptions + +#### Core Application Files + +**REMUX_GUI.py** +- Tkinter-based graphical user interface +- Multi-threaded file processing +- Queue-based progress communication +- Visual progress tracking with table display +- Automatic language detection and display +- Quick selection buttons for convenience + +**REMUX_Script.py** +- Rich library-powered CLI with beautiful formatting +- Interactive prompts for all settings +- Live updating progress table +- Comprehensive logging to file +- Tab-separated log format for easy parsing +- Batch progress tracking with summaries + +#### Documentation Files + +**README.md** +- Comprehensive project documentation +- Installation and usage instructions +- Feature descriptions and examples +- Troubleshooting guide + +**docs/FAQ.md** +- Common questions and answers +- Quick reference for common issues + +**docs/INSTALLATION.md** +- Platform-specific installation steps +- System requirements +- Verification procedures + +**docs/USAGE.md** +- Detailed usage examples +- Language code reference +- Advanced scenarios + +#### Configuration Files + +**requirements.txt** +- Python package dependencies +- Currently only requires: `rich` + +**.gitignore** +- Excludes Python cache files (`__pycache__`) +- Excludes compiled bytecode (`.pyc`, `.pyo`) +- Excludes IDE-specific files + +#### Community Files + +**LICENSE** +- MIT License - permissive open-source license +- Allows commercial and private use + +**CONTRIBUTING.md** +- Guidelines for contributing to the project +- Code style and pull request process + +**CODE_OF_CONDUCT.md** +- Community standards and expectations +- Reporting guidelines + +**SECURITY.md** +- Security policy +- How to report vulnerabilities + +**CHANGELOG.md** +- Version history +- Feature additions and bug fixes + +## πŸ”§ Prerequisites + +### System Requirements + +#### Minimum Requirements +- **Operating System**: Windows 10, macOS 10.14+, or Linux (Ubuntu 18.04+) +- **RAM**: 4 GB (8 GB recommended for large files) +- **Disk Space**: 500 MB for software + space for remuxed files +- **Processor**: Any modern CPU (multi-core recommended for batch processing) + +#### Software Dependencies + +1. **Python 3.6 or Higher** + - Required for running the scripts + - Download from [python.org](https://www.python.org/downloads/) + - Verify installation: `python --version` or `python3 --version` + +2. **MKVToolNix (mkvmerge)** + - Core dependency for MKV file processing + - Download from [mkvtoolnix.download](https://mkvtoolnix.download/) + - Version 40.0.0 or higher recommended + - Must be accessible via system PATH or default installation location + +3. **Python Packages** + - **rich**: For CLI version only (installed via pip) + - **tkinter**: For GUI version (usually included with Python) + +### Checking Prerequisites + +```bash +# Check Python version +python --version +# or +python3 --version + +# Check if mkvmerge is installed +mkvmerge --version + +# Check if tkinter is available (for GUI) +python -m tkinter +# A small window should appear if tkinter is installed + +# Check if rich is installed (for CLI) +python -c "import rich; print(rich.__version__)" +``` + +## πŸ“₯ Installation + +### Step-by-Step Installation Guide + +#### For Windows + +**1. Install Python** +```powershell +# Download Python from https://www.python.org/downloads/ +# During installation: +# βœ“ Check "Add Python to PATH" +# βœ“ Check "Install pip" + +# Verify installation +python --version +pip --version +``` + +**2. Install MKVToolNix** +```powershell +# Download from https://mkvtoolnix.download/downloads.html#windows +# Run the installer (mkvtoolnix-64-bit-XX.X.X-setup.exe) +# Follow the installation wizard +# Default location: C:\Program Files\MKVToolNix\ + +# Verify installation +mkvmerge --version + +# If not found, add to PATH: +# 1. Open System Properties > Environment Variables +# 2. Edit "Path" under System Variables +# 3. Add: C:\Program Files\MKVToolNix\ +# 4. Restart Command Prompt +``` + +**3. Clone the Repository** +```powershell +# Install Git if not already installed +# Download from: https://git-scm.com/download/win + +# Clone the repository +git clone https://github.com/mlbkumar9/Project_13.git +cd Project_13 +``` + +**4. Install Python Dependencies** +```powershell +# For CLI version (required) +pip install -r requirements.txt + +# For GUI version (tkinter usually comes with Python) +# If tkinter is missing, reinstall Python with tcl/tk support +``` + +**5. Verify Installation** +```powershell +# Test CLI version +cd "REMUX Python Scripts" +python REMUX_Script.py --help + +# Test GUI version +python REMUX_GUI.py +``` + +#### For macOS + +**1. Install Python** +```bash +# Option 1: Using Homebrew (recommended) +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +brew install python@3 + +# Option 2: Download from python.org +# Visit https://www.python.org/downloads/macos/ + +# Verify installation +python3 --version +pip3 --version +``` + +**2. Install MKVToolNix** +```bash +# Option 1: Using Homebrew (recommended) +brew install mkvtoolnix + +# Option 2: Download DMG from https://mkvtoolnix.download/downloads.html#macos +# Open the .dmg file +# Drag MKVToolNix to Applications folder +# Add to PATH in ~/.zshrc or ~/.bash_profile: +# export PATH="/Applications/MKVToolNix.app/Contents/MacOS:$PATH" + +# Verify installation +mkvmerge --version +``` + +**3. Clone the Repository** +```bash +# Install Git (if not already installed) +brew install git + +# Clone the repository +git clone https://github.com/mlbkumar9/Project_13.git +cd Project_13 +``` + +**4. Install Python Dependencies** +```bash +# For CLI version +pip3 install -r requirements.txt + +# tkinter should be included with Python +# If missing: brew install python-tk@3.x +``` + +**5. Verify Installation** +```bash +# Test CLI version +cd "REMUX Python Scripts" +python3 REMUX_Script.py + +# Test GUI version +python3 REMUX_GUI.py +``` + +#### For Linux (Ubuntu/Debian) + +**1. Install Python** +```bash +# Update package list +sudo apt update + +# Install Python and pip +sudo apt install python3 python3-pip python3-tk + +# Verify installation +python3 --version +pip3 --version +``` + +**2. Install MKVToolNix** +```bash +# Add MKVToolNix repository (for latest version) +sudo add-apt-repository ppa:ubuntuhandbook1/apps +sudo apt update + +# Install MKVToolNix +sudo apt install mkvtoolnix mkvtoolnix-gui + +# Verify installation +mkvmerge --version +``` + +**3. Clone the Repository** +```bash +# Install Git (if not already installed) +sudo apt install git + +# Clone the repository +git clone https://github.com/mlbkumar9/Project_13.git +cd Project_13 +``` + +**4. Install Python Dependencies** +```bash +# For CLI version +pip3 install -r requirements.txt + +# Or system-wide +sudo pip3 install -r requirements.txt +``` + +**5. Verify Installation** +```bash +# Test CLI version +cd "REMUX Python Scripts" +python3 REMUX_Script.py + +# Test GUI version +python3 REMUX_GUI.py +``` + +#### For Linux (Fedora/RHEL) + +**1. Install Python** +```bash +# Install Python and pip +sudo dnf install python3 python3-pip python3-tkinter + +# Verify installation +python3 --version +pip3 --version +``` + +**2. Install MKVToolNix** +```bash +# Install MKVToolNix +sudo dnf install mkvtoolnix mkvtoolnix-gui + +# Verify installation +mkvmerge --version +``` + +**3. Clone and Setup** +```bash +# Install Git +sudo dnf install git + +# Clone the repository +git clone https://github.com/mlbkumar9/Project_13.git +cd Project_13 + +# Install Python dependencies +pip3 install --user -r requirements.txt + +# Verify installation +cd "REMUX Python Scripts" +python3 REMUX_Script.py +python3 REMUX_GUI.py +``` + +### Post-Installation Verification + +Run these commands to ensure everything is working: + +```bash +# Check all dependencies +python3 --version # Should show Python 3.6+ +mkvmerge --version # Should show MKVToolNix version +python3 -c "import rich" # Should complete without errors +python3 -m tkinter # Should open a test window + +# Quick test (CLI) +cd "REMUX Python Scripts" +python3 REMUX_Script.py + +# Quick test (GUI) +python3 REMUX_GUI.py +``` + +### Installation Troubleshooting + +**Python not found:** +- Ensure Python is added to system PATH +- On Windows, reinstall Python with "Add to PATH" checked +- On macOS/Linux, use `python3` instead of `python` + +**mkvmerge not found:** +- Check if MKVToolNix is installed: find installation directory +- Add installation directory to system PATH +- On Windows: Usually `C:\Program Files\MKVToolNix\` +- On macOS: `/Applications/MKVToolNix.app/Contents/MacOS/` +- On Linux: Should be in `/usr/bin/` after installation + +**tkinter not available:** +- Windows: Reinstall Python with tcl/tk support +- macOS: `brew install python-tk` +- Ubuntu/Debian: `sudo apt install python3-tk` +- Fedora: `sudo dnf install python3-tkinter` + +**Permission denied errors:** +- Use `sudo` for system-wide installations (Linux/macOS) +- Or use `pip3 install --user` for user-only installation +- On Windows, run Command Prompt as Administrator + +## πŸ“– Usage + +### GUI Version (REMUX_GUI.py) + +#### Launching the Application + +**Windows:** +```powershell +cd "REMUX Python Scripts" +python REMUX_GUI.py +``` + +**macOS/Linux:** +```bash +cd "REMUX Python Scripts" +python3 REMUX_GUI.py +``` + +#### Step-by-Step GUI Walkthrough + +**Step 1: Select Directories** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Input Directory: [C:\Videos\Source] [Browse...] β”‚ +β”‚ Output Directory: [C:\Videos\Remuxed] [Browse...] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +- Click **Browse** next to "Input" to select the folder containing your MKV files +- Click **Browse** next to "Output" to select where remuxed files will be saved +- Output directory will be created automatically if it doesn't exist + +**Step 2: Review Available Languages** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Available Languages in MKV Files: β”‚ +β”‚ Audio: eng, jpn, spa, fra β”‚ +β”‚ Subtitles: eng, jpn, spa, fra, ger β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +- After selecting input directory, available languages are automatically detected +- This shows all audio and subtitle languages found across ALL files + +**Step 3: Select Languages** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Audio Languages: [eng,jpn] [Select All] [Clear] β”‚ +β”‚ Subtitle Languages: [eng,jpn] [Select All] [Clear] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +- Enter ISO 639-2 codes (comma-separated) for languages you want to keep +- Use **Select All** buttons to include all detected languages +- Use **Clear** buttons to reset the selection + +**Step 4: Configure Options** +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β˜‘ Skip if output exists (recommended) β”‚ +β”‚ ☐ Dry-run only (preview without processing) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +- **Skip if output exists**: Prevents re-processing already remuxed files +- **Dry-run**: Shows what would happen without actually processing files + +**Step 5: Start Processing** +- Click the **Start!** button to begin processing +- Progress table will show real-time updates for each file +- You can monitor: + - Filename + - Progress bar + - Percentage (0-100%) + - Elapsed time + - Estimated remaining time + - Status (Pending β†’ Processing β†’ OK/Failed/Skipped) + +**Step 6: Review Results** +``` +Processing complete! +Files processed: 10 +Successful: 9 +Failed: 1 +``` + +#### GUI Tips & Best Practices + +1. **Test with Dry-Run**: Always test with dry-run mode first on a few files +2. **Use Skip Option**: Enable "Skip if output exists" for resumable batch processing +3. **Monitor Progress**: Watch the progress table for any failed files +4. **Language Validation**: Only include languages that appear in "Available Languages" +5. **Free Space**: Ensure output directory has enough free disk space + +### CLI Version (REMUX_Script.py) + +#### Launching the Application + +**Windows:** +```powershell +cd "REMUX Python Scripts" +python REMUX_Script.py +``` + +**macOS/Linux:** +```bash +cd "REMUX Python Scripts" +python3 REMUX_Script.py +``` + +#### Interactive Prompts Guide + +**Prompt 1: Input Directory** +``` +Enter input directory (containing MKV files): /home/user/Videos/Source +``` +- Enter the full path to the folder containing your MKV files +- Use absolute paths for reliability +- Windows example: `C:\Videos\Source` +- Linux/macOS example: `/home/user/Videos/Source` + +**Prompt 2: Output Directory** +``` +Enter output directory (for remuxed files): /home/user/Videos/Remuxed +``` +- Enter the full path where remuxed files should be saved +- Directory will be created if it doesn't exist +- Can be the same as input directory (files will have same names) + +**Prompt 3: Language Scanning** +``` +Scanning files for available languages... + +Available Audio Languages: eng, jpn, spa +Available Subtitle Languages: eng, jpn, spa, fra, ger +``` +- Automatic scanning shows all detected languages +- This helps you know which languages are available + +**Prompt 4: Audio Language Selection** +``` +Enter audio languages to keep (comma-separated, e.g., 'eng,jpn') or press Enter for all: eng,jpn +``` +- Enter desired language codes separated by commas +- Press **Enter** without input to keep ALL detected languages +- Example: `eng,jpn,kor` +- Invalid languages trigger a warning but are still accepted + +**Prompt 5: Subtitle Language Selection** +``` +Enter subtitle languages to keep (comma-separated, e.g., 'eng,jpn') or press Enter for all: eng +``` +- Same as audio selection +- Press **Enter** for all languages +- Example: `eng,spa` + +**Prompt 6: Skip Existing Files** +``` +Skip files that already exist in output directory? (Y/n): Y +``` +- **Y** or **Enter**: Skip existing files (default, recommended) +- **n**: Re-process all files even if they exist + +**Prompt 7: Dry-Run Mode** +``` +Dry-run only? (preview without processing) (y/N): N +``` +- **y**: Preview mode - shows what would happen without processing +- **N** or **Enter**: Normal mode - actually process files (default) + +**Prompt 8: Processing Begins** +``` +β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SL β”‚ Filename β”‚ Progress β”‚ % β”‚ Elapsed β”‚ Remaining β”‚ Status β”‚ Result β”‚ +β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 1 β”‚ movie1.mkv β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ β”‚ 75% β”‚ 00:45 β”‚ 00:15 β”‚ Processing β”‚ β”‚ +β”‚ 2 β”‚ movie2.mkv β”‚ β”‚ 0% β”‚ 00:00 β”‚ --:-- β”‚ Pending β”‚ β”‚ +β”‚ 3 β”‚ movie3.mkv β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚ 100% β”‚ 01:23 β”‚ 00:00 β”‚ OK β”‚ βœ“ β”‚ +β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ Batch Progress β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘ β”‚ 40% β”‚ 02:08 β”‚ 03:12 β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` +- Live updating table shows progress for all files +- Batch progress row at bottom shows overall completion +- βœ“ indicates successful completion, βœ— indicates failure + +**Processing Complete** +``` +Summary: +Files processed: 10 +Successful: 9 +Failed: 1 + +Log file created: /home/user/Videos/Remuxed/remux_log.txt +``` + +#### CLI Tips & Best Practices + +1. **Use Full Paths**: Avoid relative paths to prevent confusion +2. **Test First**: Use dry-run mode (`y`) on first run to verify settings +3. **Check Log**: Review `remux_log.txt` in output directory for detailed information +4. **Resume Processing**: Use skip existing files to resume interrupted batches +5. **Language Codes**: Refer to ISO 639-2 language codes table below + +### Common Usage Patterns + +#### Keep Only English Audio and Subtitles +``` +Audio languages: eng +Subtitle languages: eng +``` + +#### Keep Multiple Languages +``` +Audio languages: eng,jpn,kor +Subtitle languages: eng,jpn,kor +``` + +#### Keep All Available Languages +``` +Audio languages: [Press Enter] +Subtitle languages: [Press Enter] +``` + +#### Preview Before Processing +``` +Dry-run only? (y/N): y +``` + +#### Process New Files Only +``` +Skip files that already exist? (Y/n): Y +``` + +## 🌍 ISO 639-2 Language Codes Reference + +Complete reference table for common languages. Use these three-letter codes when selecting languages. + +| Language | ISO 639-2 Code | Language | ISO 639-2 Code | +|----------|----------------|----------|----------------| +| **English** | **eng** | **Japanese** | **jpn** | +| **Spanish** | **spa** | **Korean** | **kor** | +| **French** | **fra** | **Chinese** | **chi/zho** | +| **German** | **deu/ger** | **Portuguese** | **por** | +| **Italian** | **ita** | **Russian** | **rus** | +| **Dutch** | **dut/nld** | **Arabic** | **ara** | +| **Polish** | **pol** | **Turkish** | **tur** | +| **Swedish** | **swe** | **Hindi** | **hin** | +| **Norwegian** | **nor** | **Thai** | **tha** | +| **Danish** | **dan** | **Vietnamese** | **vie** | +| **Finnish** | **fin** | **Hebrew** | **heb** | +| **Czech** | **cze/ces** | **Indonesian** | **ind** | +| **Greek** | **gre/ell** | **Malay** | **may/msa** | +| **Hungarian** | **hun** | **Tamil** | **tam** | +| **Romanian** | **rum/ron** | **Telugu** | **tel** | +| **Ukrainian** | **ukr** | **Bengali** | **ben** | +| **Croatian** | **hrv** | **Marathi** | **mar** | +| **Slovak** | **slo/slk** | **Gujarati** | **guj** | +| **Bulgarian** | **bul** | **Kannada** | **kan** | +| **Serbian** | **srp** | **Malayalam** | **mal** | +| **Catalan** | **cat** | **Punjabi** | **pan** | +| **Lithuanian** | **lit** | **Urdu** | **urd** | +| **Slovenian** | **slv** | **Persian/Farsi** | **per/fas** | +| **Latvian** | **lav** | **Swahili** | **swa** | +| **Estonian** | **est** | **Filipino/Tagalog** | **fil/tgl** | +| **Icelandic** | **ice/isl** | **Burmese** | **bur/mya** | +| **Irish** | **gle** | **Khmer** | **khm** | +| **Albanian** | **alb/sqi** | **Lao** | **lao** | +| **Macedonian** | **mac/mkd** | **Nepali** | **nep** | +| **Bosnian** | **bos** | **Sinhala** | **sin** | +| **Basque** | **baq/eus** | **Mongolian** | **mon** | +| **Galician** | **glg** | **Kazakh** | **kaz** | +| **Welsh** | **wel/cym** | **Uzbek** | **uzb** | +| **Afrikaans** | **afr** | **Azerbaijani** | **aze** | +| **Belarusian** | **bel** | **Georgian** | **geo/kat** | +| **Armenian** | **arm/hya** | **Amharic** | **amh** | +| **Maltese** | **mlt** | **Pashto** | **pus** | +| **Yiddish** | **yid** | **Kurdish** | **kur** | +| **Esperanto** | **epo** | **Hausa** | **hau** | +| **Latin** | **lat** | **Somali** | **som** | +| **Undetermined** | **und** | **Multiple Languages** | **mul** | + +### Notes on Language Codes + +1. **Alternative Codes**: Some languages have two valid ISO 639-2 codes: + - **German**: `deu` (bibliographic) or `ger` (terminologic) - both are valid + - **French**: `fra` is standard + - **Chinese**: `chi` (bibliographic) or `zho` (terminologic) + - Use whichever code appears in your files (check "Available Languages") + +2. **Special Codes**: + - **und**: Undetermined/undefined language (commonly found in MKV files) + - **mul**: Multiple languages in one track + - **zxx**: No linguistic content (e.g., music, sound effects) + +3. **Case Sensitivity**: Codes are case-insensitive + - `ENG`, `eng`, and `Eng` are all equivalent + - The application automatically converts to lowercase + +4. **Finding Language Codes**: + - Run the application and check "Available Languages" to see codes in your files + - Official ISO 639-2 list: [Library of Congress](https://www.loc.gov/standards/iso639-2/php/code_list.php) + +### Examples of Language Selection + +**Keep English and Japanese only:** +``` +Audio: eng,jpn +Subtitles: eng,jpn +``` + +**Keep multiple European languages:** +``` +Audio: eng,fra,deu,spa,ita +Subtitles: eng,fra,deu,spa,ita +``` + +**Keep Asian languages:** +``` +Audio: jpn,kor,chi +Subtitles: jpn,kor,chi,eng +``` + +**Include undefined language tracks:** +``` +Audio: eng,und +Subtitles: eng,und +``` + +## πŸ”„ Workflow and Data Flow + +### Overall Workflow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ START: User Launch Application β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Interface Selection β”‚ + β”‚ GUI or CLI? β”‚ + β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ GUI Mode β”‚ β”‚ CLI Mode β”‚ + β”‚ (Tkinter) β”‚ β”‚ (Rich) β”‚ + β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Get User Settings β”‚ + β”‚ - Input directory β”‚ + β”‚ - Output directory β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Validate Directories β”‚ + β”‚ - Check existence β”‚ + β”‚ - Check permissions β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Find MKV Files β”‚ + β”‚ in Input Directory β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Scan All Files for β”‚ + β”‚ Available Languages β”‚ + β”‚ (Audio & Subtitles) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Display Available β”‚ + β”‚ Languages to User β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ User Selects β”‚ + β”‚ Desired Languages β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ User Sets Options β”‚ + β”‚ - Skip existing? β”‚ + β”‚ - Dry-run mode? β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Start Processing β”‚ + β”‚ (Loop through files) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ FOR EACH MKV FILE: β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Check if Output β”‚ + β”‚ Already Exists β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ + β”‚ β”‚ + [Exists & [Doesn't exist + Skip=Yes] or Skip=No] + β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β” β”‚ + β”‚ SKIP β”‚ β”‚ + β”‚ FILE β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Identify Tracks β”‚ + β”‚ β”‚ using mkvmerge β”‚ + β”‚ β”‚ (JSON format) β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Filter Tracks by β”‚ + β”‚ β”‚ Selected Langs β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Build mkvmerge β”‚ + β”‚ β”‚ Command β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Execute Command β”‚ + β”‚ β”‚ [Dry-run?] β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Parse Progress β”‚ + β”‚ β”‚ Update UI β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ Check Exit Code β”‚ + β”‚ β”‚ Validate Output β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ + └───────────┴─────┐ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Update Status β”‚ + β”‚ (OK/Failed) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ END OF LOOP - All files processed β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Show Summary β”‚ + β”‚ - Total files β”‚ + β”‚ - Successful β”‚ + β”‚ - Failed β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Create Log β”‚ + β”‚ (CLI only) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ END: Processing Complete β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Data Flow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User Input β”‚ +β”‚ - Dirs β”‚ +β”‚ - Languages β”‚ +β”‚ - Options β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Application Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Input Validation β”‚ β”‚ +β”‚ β”‚ - Directory exists? β”‚ β”‚ +β”‚ β”‚ - Permissions OK? β”‚ β”‚ +β”‚ β”‚ - MKV files present? β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Language Detection β”‚ β”‚ +β”‚ β”‚ Input: List of MKV files β”‚ β”‚ +β”‚ β”‚ Process: mkvmerge --identify (JSON) β”‚ β”‚ +β”‚ β”‚ Output: Available audio/sub languages β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ User Language Selection β”‚ β”‚ +β”‚ β”‚ Input: Available languages β”‚ β”‚ +β”‚ β”‚ Process: User chooses languages β”‚ β”‚ +β”‚ β”‚ Output: Selected language list β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ File Processing Loop β”‚ β”‚ +β”‚ β”‚ For each MKV file: β”‚ β”‚ +β”‚ β”‚ 1. Read track info (JSON) β”‚ β”‚ +β”‚ β”‚ 2. Filter by selected languages β”‚ β”‚ +β”‚ β”‚ 3. Build command β”‚ β”‚ +β”‚ β”‚ 4. Execute mkvmerge β”‚ β”‚ +β”‚ β”‚ 5. Monitor progress β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ MKVToolNix Layer β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ mkvmerge Process β”‚ β”‚ +β”‚ β”‚ Input: Source MKV + Command β”‚ β”‚ +β”‚ β”‚ Process: β”‚ β”‚ +β”‚ β”‚ - Read source file β”‚ β”‚ +β”‚ β”‚ - Extract video tracks (all) β”‚ β”‚ +β”‚ β”‚ - Extract selected audio tracks β”‚ β”‚ +β”‚ β”‚ - Extract selected subtitle tracks β”‚ β”‚ +β”‚ β”‚ - Mux into new container β”‚ β”‚ +β”‚ β”‚ Output: Remuxed MKV file β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Progress & Status Flow β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Progress Data β”‚ β”‚ +β”‚ β”‚ - Percentage (0-100%) β”‚ β”‚ +β”‚ β”‚ - Elapsed time β”‚ β”‚ +β”‚ β”‚ - Estimated remaining time β”‚ β”‚ +β”‚ β”‚ - Current status β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ GUI β”‚ β”‚ CLI β”‚ β”‚ +β”‚ β”‚ Queue β”‚ β”‚ Table β”‚ β”‚ +β”‚ β”‚ Updates β”‚ β”‚ Updates β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Output Files β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Remuxed MKV Files β”‚ β”‚ +β”‚ β”‚ - Same video quality β”‚ β”‚ +β”‚ β”‚ - Selected audio languages β”‚ β”‚ +β”‚ β”‚ - Selected subtitle languages β”‚ β”‚ +β”‚ β”‚ - Reduced file size (usually) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Log File (CLI only) β”‚ β”‚ +β”‚ β”‚ - Processing timestamp β”‚ β”‚ +β”‚ β”‚ - Source/destination paths β”‚ β”‚ +β”‚ β”‚ - Languages selected β”‚ β”‚ +β”‚ β”‚ - Commands executed β”‚ β”‚ +β”‚ β”‚ - Success/failure status β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Track Selection Logic + +``` +Input MKV File +β”œβ”€β”€ Video Tracks +β”‚ β”œβ”€β”€ Track 0: H.264 video ──────────────┐ +β”‚ └── Track 1: H.264 video (commentary) ─── +β”‚ β”‚ +β”œβ”€β”€ Audio Tracks β”‚ ALL video tracks +β”‚ β”œβ”€β”€ Track 2: Japanese (jpn) ────┐ β”‚ always included +β”‚ β”œβ”€β”€ Track 3: English (eng) ────── β”‚ β”‚ +β”‚ β”œβ”€β”€ Track 4: Spanish (spa) β”‚ β”‚ β”‚ +β”‚ └── Track 5: French (fra) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +└── Subtitle Tracks Selected β”‚ β”‚ + β”œβ”€β”€ Track 6: English (eng) ────┐ based β”‚ β”‚ + β”œβ”€β”€ Track 7: Spanish (spa) β”‚ on β”‚ β”‚ + β”œβ”€β”€ Track 8: French (fra) ────── user β”‚ β”‚ + └── Track 9: Japanese (jpn) β”‚ input β”‚ β”‚ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ mkvmerge Command β”‚ + β”‚ --audio-tracks 2,3 β”‚ + β”‚ --subtitle-tracks 6,8 β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + Output MKV File + β”œβ”€β”€ Video Track 0 + β”œβ”€β”€ Video Track 1 + β”œβ”€β”€ Audio Track 2 (jpn) + β”œβ”€β”€ Audio Track 3 (eng) + β”œβ”€β”€ Subtitle Track 6 (eng) + └── Subtitle Track 8 (fra) +``` + +## πŸ’‘ Practical Usage Examples + +### Example 1: Anime Collection - Keep Japanese Audio and English Subtitles + +**Scenario**: You have a large anime collection with multiple audio and subtitle tracks. You want to keep Japanese audio and English subtitles only to save space. + +**Setup**: +- Input: `/home/user/Anime/Raw/` (100 GB of anime with multiple languages) +- Output: `/home/user/Anime/Cleaned/` +- Goal: Reduce file size by ~30-40% while keeping essential tracks + +**GUI Steps**: +1. Launch `REMUX_GUI.py` +2. Set Input: `/home/user/Anime/Raw/` +3. Set Output: `/home/user/Anime/Cleaned/` +4. Wait for language detection +5. Audio Languages: `jpn` +6. Subtitle Languages: `eng` +7. βœ“ Skip if output exists +8. ☐ Dry-run (uncheck for actual processing) +9. Click **Start!** + +**CLI Steps**: +```bash +python3 REMUX_Script.py + +Enter input directory: /home/user/Anime/Raw/ +Enter output directory: /home/user/Anime/Cleaned/ +# (automatic scanning) +Audio languages: jpn +Subtitle languages: eng +Skip existing? Y +Dry-run? N +``` + +**Expected Result**: +- Original: 50 files Γ— 2 GB = 100 GB +- Cleaned: 50 files Γ— 1.3 GB = 65 GB +- Space saved: 35 GB +- Processing time: ~20-30 minutes (depending on CPU) + +--- + +### Example 2: International Movie Library - Multiple Languages + +**Scenario**: You maintain a movie library for a multilingual family. You need English, Spanish, and French audio and subtitles, removing all others. + +**Setup**: +- Input: `C:\Movies\Downloads\` +- Output: `C:\Movies\Library\` +- Languages needed: English, Spanish, French +- Files: Mixed content with 8-10 language tracks each + +**GUI Steps**: +1. Launch `REMUX_GUI.py` +2. Set Input: `C:\Movies\Downloads\` +3. Set Output: `C:\Movies\Library\` +4. Review detected languages (might show: eng, spa, fra, deu, ita, jpn, por, rus) +5. Audio Languages: `eng,spa,fra` +6. Subtitle Languages: `eng,spa,fra` +7. βœ“ Skip if output exists +8. Click **Start!** + +**Expected Result**: +- Each file reduced from 8-10 tracks to 3-4 tracks +- File size reduction: 20-35% per file +- Library remains fully functional for target languages + +--- + +### Example 3: Video Archive - Testing Before Batch Processing + +**Scenario**: You have 500+ video files and want to ensure settings are correct before processing them all. + +**Setup**: +- Create a test subset first +- Use dry-run mode to verify +- Process test batch +- Review results before full batch + +**Steps**: +```bash +# Step 1: Create test directory with 5 sample files +mkdir /tmp/test_input +cp /path/to/archive/sample*.mkv /tmp/test_input/ + +# Step 2: Dry-run test +python3 REMUX_Script.py +Input: /tmp/test_input +Output: /tmp/test_output +Audio: eng,jpn +Subtitles: eng +Skip existing? Y +Dry-run? y # <-- DRY RUN MODE + +# Step 3: Review dry-run output +# Verify which tracks would be selected +# Check for any errors or warnings + +# Step 4: Actual test run on 5 files +# Repeat with Dry-run? N + +# Step 5: Verify output files +ffprobe /tmp/test_output/sample1.mkv # Check tracks +vlc /tmp/test_output/sample1.mkv # Test playback + +# Step 6: Full batch processing (if test successful) +python3 REMUX_Script.py +Input: /path/to/archive/ +Output: /path/to/archive_cleaned/ +# ... same settings as test +``` + +**Best Practice**: Always test on a small subset before processing large batches! + +--- + +### Example 4: Resumable Large Batch - Handling Interruptions + +**Scenario**: You're processing 1000 files, but your computer crashes or you need to stop midway. You want to resume without reprocessing completed files. + +**Setup**: +- Large batch processing (overnight job) +- Potential interruptions (power, system updates, etc.) +- Need resumable processing + +**CLI Steps**: +```bash +# Initial run +python3 REMUX_Script.py +Input: /media/external/Videos/ +Output: /media/external/Videos_Processed/ +Audio: eng +Subtitles: eng,spa +Skip existing? Y # <-- CRITICAL for resumable processing +Dry-run? N + +# ... processes 347/1000 files before interruption ... + +# Resume after interruption (next day) +python3 REMUX_Script.py +# Use EXACT same settings +Input: /media/external/Videos/ +Output: /media/external/Videos_Processed/ # Same output! +Audio: eng +Subtitles: eng,spa +Skip existing? Y # <-- Will skip the 347 already processed + +# Result: Only remaining 653 files are processed +``` + +**Key Points**: +- "Skip existing" must be enabled +- Use same output directory +- Previously completed files are automatically skipped +- Check log file to see which files were skipped vs. newly processed + +--- + +### Example 5: Corporate Training Videos - Standardization + +**Scenario**: Company has training videos in various formats and languages. HR wants to standardize them: English audio only, English and Spanish subtitles. + +**Setup**: +- Input: Mixed video files from various departments +- Output: Standardized corporate library +- Requirement: English audio + English & Spanish subtitles only +- Compliance: Keep log for audit trail + +**Workflow**: + +**Phase 1: Assessment** +```bash +# Step 1: Scan files to see what languages exist +python3 REMUX_Script.py +Input: /company/training/raw/ +# Check "Available Languages" output +# Document findings for stakeholders +``` + +**Phase 2: Test Run** +```bash +# Step 2: Test with dry-run +Output: /company/training/test/ +Audio: eng +Subtitles: eng,spa +Dry-run? y +# Review dry-run results +``` + +**Phase 3: Production Run** +```bash +# Step 3: Actual processing +Output: /company/training/standardized/ +Audio: eng +Subtitles: eng,spa +Skip existing? Y +Dry-run? N +``` + +**Phase 4: Documentation** +```bash +# Step 4: Archive the log for compliance +cp /company/training/standardized/remux_log.txt \ + /company/training/audit_logs/remux_$(date +%Y%m%d).txt + +# Step 5: Generate summary report +awk -F'\t' '{print $NF}' /company/training/standardized/remux_log.txt | \ +sort | uniq -c +# Shows count of OK, Failed, Skipped files +``` + +**Deliverables**: +- Standardized video library +- Audit log showing all transformations +- Summary report for management +- Reduced storage costs (typically 30-50% reduction) + +--- + +### Common Patterns Across Examples + +**Space Savings**: +- Removing 5-6 language tracks: 40-50% size reduction +- Removing 2-3 language tracks: 20-30% size reduction +- Keeping only 1 audio + 1 subtitle: Maximum reduction + +**Processing Speed** (rough estimates): +- 1 GB file: 30-60 seconds +- 10 GB file: 5-10 minutes +- 100 GB batch: 1-2 hours +- 1 TB batch: 10-20 hours + +**Recommended Workflow**: +1. Always test with dry-run first +2. Process a small test batch (3-5 files) +3. Verify output quality and completeness +4. Process full batch with "skip existing" enabled +5. Keep logs for future reference + +## πŸ”§ Troubleshooting + +Comprehensive guide to resolving common issues with MKV Batch Remuxer. + +### 1. mkvmerge Not Found + +**Symptoms**: +``` +Error: mkvmerge not found +Please install MKVToolNix +``` + +**Causes**: +- MKVToolNix not installed +- MKVToolNix not in system PATH +- Incorrect installation + +**Solutions**: + +**Windows**: +```powershell +# Check if installed +where mkvmerge + +# If not found, add to PATH manually: +# 1. Find installation directory (usually C:\Program Files\MKVToolNix\) +# 2. Add to System PATH: +# - Right-click "This PC" β†’ Properties +# - Advanced system settings β†’ Environment Variables +# - Edit "Path" under System variables +# - Add: C:\Program Files\MKVToolNix\ +# - Click OK and restart Command Prompt + +# Alternative: Reinstall MKVToolNix +# Download from https://mkvtoolnix.download/downloads.html +``` + +**macOS**: +```bash +# Check if installed +which mkvmerge + +# Install via Homebrew (recommended) +brew install mkvtoolnix + +# Or add to PATH if installed via DMG +echo 'export PATH="/Applications/MKVToolNix.app/Contents/MacOS:$PATH"' >> ~/.zshrc +source ~/.zshrc +``` + +**Linux**: +```bash +# Check if installed +which mkvmerge + +# Ubuntu/Debian +sudo apt update +sudo apt install mkvtoolnix + +# Fedora/RHEL +sudo dnf install mkvtoolnix +``` + +--- + +### 2. Python Not Recognized + +**Symptoms**: +``` +'python' is not recognized as an internal or external command +python: command not found +``` + +**Solutions**: + +**Windows**: +```powershell +# Check Python installation +python --version +# or +py --version + +# If not found, reinstall Python +# Download from https://www.python.org/downloads/ +# During installation: βœ“ Check "Add Python to PATH" +``` + +**macOS/Linux**: +```bash +# Use python3 instead of python +python3 --version + +# Or create alias +echo 'alias python=python3' >> ~/.bashrc # Linux +echo 'alias python=python3' >> ~/.zshrc # macOS +source ~/.bashrc # or ~/.zshrc +``` + +--- + +### 3. No MKV Files Found + +**Symptoms**: +``` +Error: No MKV files found in input directory +``` + +**Causes**: +- Wrong directory selected +- Files have different extension (.mkv vs .MKV) +- Files in subdirectories (not searched recursively) + +**Solutions**: +```bash +# Verify files exist +ls -la /path/to/directory/*.mkv + +# Check for uppercase extensions +ls -la /path/to/directory/*.MKV + +# If files are in subdirectories, move them to main folder +find /path/to/directory -name "*.mkv" -exec mv {} /path/to/directory/ \; + +# Or copy instead of move +find /path/to/directory -name "*.mkv" -exec cp {} /path/to/directory/ \; +``` + +--- + +### 4. Permission Denied Errors + +**Symptoms**: +``` +PermissionError: [Errno 13] Permission denied +Cannot write to output directory +``` + +**Solutions**: + +**Windows**: +```powershell +# Run as Administrator +# Right-click Command Prompt β†’ "Run as administrator" + +# Or change folder permissions +# Right-click output folder β†’ Properties β†’ Security +# Give your user account "Full control" +``` + +**macOS/Linux**: +```bash +# Check permissions +ls -ld /path/to/output + +# Fix permissions +chmod 755 /path/to/output + +# Or use sudo (not recommended for normal operation) +sudo python3 REMUX_Script.py + +# Better: Change ownership to your user +sudo chown -R $USER:$USER /path/to/output +``` + +--- + +### 5. GUI Window Not Appearing + +**Symptoms**: +- GUI script runs but no window appears +- Error: `ModuleNotFoundError: No module named 'tkinter'` + +**Solutions**: + +**Windows**: +```powershell +# Reinstall Python with tcl/tk support +# Download and run Python installer +# Choose "Modify" and ensure tcl/tk is selected +``` + +**macOS**: +```bash +# Install tkinter +brew install python-tk@3.11 # Replace 3.11 with your Python version +``` + +**Linux**: +```bash +# Ubuntu/Debian +sudo apt install python3-tk + +# Fedora +sudo dnf install python3-tkinter + +# Test installation +python3 -m tkinter +# A small test window should appear +``` + +--- + +### 6. "Rich" Module Not Found (CLI) + +**Symptoms**: +``` +ModuleNotFoundError: No module named 'rich' +``` + +**Solutions**: +```bash +# Install rich library +pip install rich +# or +pip3 install rich + +# If permission denied +pip3 install --user rich + +# Verify installation +python3 -c "import rich; print(rich.__version__)" +``` + +--- + +### 7. Processing Extremely Slow + +**Symptoms**: +- Files taking much longer than expected +- System becomes unresponsive +- High CPU/memory usage + +**Solutions**: + +**Check System Resources**: +```bash +# Linux/macOS +top +htop # if installed + +# Windows +# Open Task Manager (Ctrl+Shift+Esc) +``` + +**Optimize Processing**: +- Close other applications +- Process files in smaller batches +- Ensure sufficient free disk space (2x largest file size) +- Check if antivirus is scanning processed files (exclude output folder) +- Use SSD for output directory if possible + +**For GUI**: Processing happens in parallel by default, which might overload older systems. + +--- + +### 8. Output File Smaller Than Expected + +**Symptoms**: +- Output file significantly smaller than input +- Missing video or audio content + +**Solutions**: + +**Check if tracks were removed**: +```bash +# Compare input and output tracks +mkvmerge -i input.mkv +mkvmerge -i output.mkv + +# Or use mediainfo +mediainfo input.mkv > input_info.txt +mediainfo output.mkv > output_info.txt +diff input_info.txt output_info.txt +``` + +**Common causes**: +- Removed more language tracks than intended +- Check selected language codes match your needs +- Verify "Available Languages" before processing + +--- + +### 9. "No Desired Audio" or Track Errors + +**Symptoms**: +``` +Status: No desired audio +Status: ID Failed +``` + +**Causes**: +- Selected language not present in file +- Typo in language code +- File corruption + +**Solutions**: + +**Verify language codes**: +```bash +# Check what languages are actually in the file +mkvmerge -i problematic_file.mkv + +# Look for "language:" field in output +``` + +**Fix approach**: +1. Use "Available Languages" display to see what's actually present +2. Only select languages that appear in "Available Languages" +3. Check for typos: `eng` not `en`, `jpn` not `jp` +4. Try including `und` (undefined) if tracks have no language tag + +--- + +### 10. Files Skipped Unexpectedly + +**Symptoms**: +- All files show "Skipped (exists)" status +- Files not processing even though they should + +**Causes**: +- "Skip if output exists" is enabled +- Previous partial run created output files + +**Solutions**: + +**GUI**: +- Uncheck "Skip if output exists" option +- Or manually delete existing output files + +**CLI**: +- Answer "n" to "Skip files that already exist?" +- Or delete output directory contents: +```bash +rm -rf /path/to/output/* # Linux/macOS +del /Q /path/to/output\* # Windows +``` + +--- + +### 11. Dry-Run Mode Confusion + +**Symptoms**: +- No files being created +- Everything shows as "Dry-run (skipped)" + +**Cause**: +- Dry-run mode is enabled (preview only) + +**Solutions**: + +**GUI**: Uncheck "Dry-run only" checkbox + +**CLI**: Answer "N" to "Dry-run only?" prompt + +--- + +### 12. GUI Freezing or Not Responding + +**Symptoms**: +- GUI becomes unresponsive during processing +- Window shows "(Not Responding)" + +**Causes**: +- Threading issue +- Very large files +- System resource exhaustion + +**Solutions**: +```bash +# Close and restart the application +# Process fewer files at once +# Monitor system resources +# Ensure adequate free RAM (2-4 GB minimum) +``` + +If persistent, use CLI version instead: +```bash +python3 REMUX_Script.py +``` + +--- + +### 13. Output File Won't Play + +**Symptoms**: +- Remuxed file won't play in media player +- Corrupted video/audio + +**Causes**: +- Processing interrupted +- Source file corrupted +- Insufficient disk space during processing + +**Solutions**: + +**Verify output file**: +```bash +# Check if file is complete +ls -lh output.mkv + +# Verify with mkvmerge +mkvmerge -i output.mkv + +# Try playing with VLC (more forgiving) +vlc output.mkv + +# Check mkvmerge error output +ffprobe output.mkv +``` + +**Recovery**: +1. Delete corrupted output file +2. Re-process source file +3. Ensure sufficient free disk space (2x source file size) +4. Check source file plays correctly before remuxing + +--- + +### 14. Wrong Language Tracks in Output + +**Symptoms**: +- Output has different languages than selected +- Unexpected tracks included + +**Causes**: +- Files have incorrect language tags +- Misunderstanding of ISO 639-2 codes + +**Solutions**: + +**Verify source file language tags**: +```bash +mkvmerge -i source.mkv +# Check "language:" field for each track +``` + +**Common issues**: +- Track tagged as "und" (undefined) when it's actually English +- Track tagged incorrectly by original encoder +- Using wrong ISO code (eng vs en) + +**Fix**: +- Include "und" in your language selection if needed +- Use MKVToolNix GUI to manually check and retag source files +- Or accept the language tags as-is and select accordingly + +--- + +### 15. Log File Not Created (CLI) + +**Symptoms**: +- No `remux_log.txt` in output directory +- Can't find processing log + +**Causes**: +- Permission issues +- Script error before log creation +- Output directory doesn't exist + +**Solutions**: +```bash +# Check output directory exists and is writable +ls -ld /path/to/output +touch /path/to/output/test.txt +rm /path/to/output/test.txt + +# Check script errors +python3 REMUX_Script.py 2>&1 | tee error_log.txt + +# Ensure output directory is created before processing starts +mkdir -p /path/to/output +``` + +--- + +### 16. Subtitle Synchronization Issues + +**Symptoms**: +- Subtitles out of sync in remuxed file +- Subtitle timing different from original + +**Cause**: +- This shouldn't happen with remuxing (no re-encoding) +- If it does, source file may have issues + +**Solutions**: +```bash +# Verify source file subtitles +vlc source.mkv # Check if subs are in sync + +# If source is fine but output is not: +# This indicates a bug - report it! + +# Workaround: Use MKVToolNix GUI manually +# Or extract subs and re-mux separately +``` + +--- + +### 17. Unsupported File Format + +**Symptoms**: +``` +Error: Not a valid MKV file +File format not supported +``` + +**Solutions**: +```bash +# Verify file is actually MKV +file suspicious_file.mkv + +# Check file isn't corrupted +mkvmerge -i suspicious_file.mkv + +# Convert to MKV if different format +ffmpeg -i input.mp4 -c copy output.mkv +``` + +--- + +### 18. Batch Progress Not Updating (CLI) + +**Symptoms**: +- Progress table not refreshing +- Terminal appears frozen but process is running + +**Causes**: +- Terminal doesn't support live updates +- Rich library compatibility issue + +**Solutions**: +```bash +# Try different terminal +# Windows: Use Windows Terminal instead of CMD +# macOS: Use iTerm2 or default Terminal +# Linux: Use gnome-terminal or konsole + +# Check Rich version +pip show rich + +# Update Rich +pip install --upgrade rich +``` + +--- + +### Getting Additional Help + +If you encounter issues not covered here: + +1. **Check the logs**: + - CLI: Review `remux_log.txt` in output directory + - Look for error messages and stack traces + +2. **Enable verbose output**: + ```bash + python3 REMUX_Script.py 2>&1 | tee debug.txt + ``` + +3. **Test with single file**: + - Isolate the problem file + - Process it individually + - Share error output when reporting issues + +4. **Gather information**: + ```bash + python3 --version + mkvmerge --version + uname -a # Linux/macOS + systeminfo # Windows + ``` + +5. **Report issues**: + - Create an issue on GitHub + - Include: OS, Python version, mkvmerge version, error message + - Provide steps to reproduce + +## ❓ FAQ + +### General Questions + +**Q: What is the difference between remuxing and re-encoding?** + +A: Remuxing changes the container format without touching the actual video/audio streams, so there's no quality loss and it's very fast. Re-encoding converts the video/audio streams themselves, which takes much longer and can reduce quality. + +**Q: Will remuxing reduce video quality?** + +A: No! Remuxing does not re-encode the video or audio streams. The quality remains exactly the same as the source. Only the container and track selection change. + +**Q: How much space can I save?** + +A: It depends on how many tracks you remove: +- Removing 5-6 language tracks: 40-50% reduction +- Removing 2-3 language tracks: 20-30% reduction +- Removing 1 language track: 5-15% reduction + +**Q: Can I undo a remux operation?** + +A: No, removed tracks cannot be recovered. Always keep your original files until you've verified the remuxed versions are correct. Use dry-run mode to preview changes first. + +**Q: Does this work with other video formats besides MKV?** + +A: No, this tool is specifically designed for MKV (Matroska) files. For other formats, consider converting to MKV first using tools like FFmpeg or HandBrake. + +--- + +### Technical Questions + +**Q: Why do I need MKVToolNix? Can't the script handle everything?** + +A: MKVToolNix's `mkvmerge` is an industry-standard tool specifically designed for MKV manipulation. It's more reliable and efficient than trying to manually parse and rebuild MKV files. This script is a user-friendly wrapper around mkvmerge. + +**Q: Can I run multiple instances simultaneously?** + +A: Yes, but be aware of system resources. Each instance will consume CPU and disk I/O. For best performance, process different directories or use different output locations. + +**Q: What's the difference between GUI and CLI versions?** + +A: +- **GUI**: Graphical interface, better for occasional use, visual progress tracking +- **CLI**: Command-line interface, better for automation, remote servers, scripting, and detailed logging + +Both have the same core functionality and use the same remuxing engine. + +**Q: Does it support HDR, Dolby Vision, or other advanced formats?** + +A: Yes! Since remuxing doesn't re-encode, all original formats are preserved including HDR10, HDR10+, Dolby Vision, Atmos, DTS:X, etc. The streams are copied as-is. + +**Q: Can this tool merge multiple MKV files into one?** + +A: No, this tool is designed for track selection/removal, not file merging. Use MKVToolNix GUI or `mkvmerge` directly for merging files. + +--- + +### Usage Questions + +**Q: How do I keep all languages except one?** + +A: Currently, you must specify which languages to keep. There's no "exclude" mode. Check "Available Languages" and list all languages except the one you want to remove. + +**Q: What does "und" mean in language codes?** + +A: "und" stands for "undefined" - it means the track has no language tag. This is common in MKV files. Include "und" in your selection if you want to keep these tracks. + +**Q: Can I process files from a network drive?** + +A: Yes, but performance will be slower due to network I/O. For best results, copy files locally, process them, then copy back to the network drive. + +**Q: Why is my processing so slow?** + +A: Processing speed depends on: +- File size (larger = slower) +- Disk speed (SSD >> HDD) +- CPU speed +- Number of simultaneous operations +- Whether you're reading/writing to the same disk + +Typical speed: 1-2 minutes per GB on modern hardware. + +**Q: Can I schedule automated batch processing?** + +A: Yes! The CLI version is perfect for automation: + +**Windows Task Scheduler**: +```powershell +# Create a batch file (remux_batch.bat): +cd "C:\Project_13\REMUX Python Scripts" +python REMUX_Script.py +# Configure answers in script or use process automation +``` + +**Linux/macOS Cron**: +```bash +# Add to crontab +0 2 * * * cd /path/to/scripts && python3 REMUX_Script.py < config.txt +``` + +--- + +### Error Questions + +**Q: Why do some files fail while others succeed?** + +A: Common reasons: +- File corruption in source +- Selected language not present in that file +- Insufficient disk space +- File locked by another process +- Permission issues + +Check the error message and `remux_log.txt` for specific details. + +**Q: The script says "No desired audio" - what does this mean?** + +A: None of your selected audio languages were found in that file. Either: +- The file has different language tracks than you selected +- Language tags are incorrect in the source file +- Typo in your language code selection + +Use "Available Languages" display to see what's actually in your files. + +**Q: Why is the output file larger than the input?** + +A: This should rarely happen, but can occur if: +- Original file was already heavily compressed/optimized +- You're keeping more tracks than the original had +- Original file had corrupt/incomplete tracks + +Usually output is same size or smaller. + +--- + +### Performance Questions + +**Q: How many files can I process at once?** + +A: There's no hard limit, but consider: +- **GUI**: Processes multiple files in parallel (uses threading) +- **CLI**: Processes files sequentially +- System resources (RAM, disk I/O) are the limiting factors + +Recommended: Start with small batches (10-20 files) and scale up based on performance. + +**Q: Will this damage my hard drive with heavy use?** + +A: No more than any other file operations. Modern SSDs and HDDs are designed for this. However: +- Ensure adequate cooling +- Don't fill disk to 100% capacity +- Consider using a separate drive for output + +**Q: Can I use this on a Raspberry Pi or low-power device?** + +A: Yes! Both versions will work, but processing will be slower. The CLI version is recommended for resource-constrained devices as it has less overhead. + +--- + +### Safety Questions + +**Q: Is it safe to delete original files after remuxing?** + +A: Only after: +1. Verifying all files processed successfully +2. Playing back several remuxed files to confirm quality +3. Checking file sizes are reasonable +4. Reviewing the log for any errors + +Best practice: Keep originals until you're 100% confident, or backup to external storage. + +**Q: What happens if processing is interrupted (power loss, crash)?** + +A: +- Incomplete files will be left in the output directory +- Enable "Skip if output exists" to resume processing +- Delete incomplete output files before resuming +- No damage to source files (they're never modified) + +**Q: Can this tool introduce viruses or malware?** + +A: No. The tool: +- Only manipulates MKV containers +- Doesn't execute code from video files +- Doesn't download anything +- Is open source (you can review the code) + +However, always download from official sources (GitHub) and verify dependencies. + +--- + +## 🀝 Contributing + +We welcome contributions to MKV Batch Remuxer! Here's how you can help: + +### Ways to Contribute + +1. **Report Bugs**: Open an issue on GitHub with details about the problem +2. **Suggest Features**: Share ideas for improvements +3. **Submit Pull Requests**: Fix bugs or add features +4. **Improve Documentation**: Help make docs clearer and more comprehensive +5. **Share Use Cases**: Tell us how you're using the tool + +### Contribution Guidelines + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on: +- Code style and standards +- Pull request process +- Testing requirements +- Documentation standards + +### Development Setup + +```bash +# Clone the repository +git clone https://github.com/mlbkumar9/Project_13.git +cd Project_13 + +# Install dependencies +pip install -r requirements.txt + +# Make your changes +# Test thoroughly + +# Submit pull request +``` + +### Code of Conduct + +We follow a [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a welcoming environment for all contributors. + +--- + +## πŸ“„ License + +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. + +### MIT License Summary + +βœ… **Permissions**: +- Commercial use +- Modification +- Distribution +- Private use + +❌ **Limitations**: +- Liability +- Warranty + +πŸ“‹ **Conditions**: +- License and copyright notice must be included + +--- + +## πŸ“ž Support + +### Getting Help + +1. **Documentation**: Read this README and docs/ folder thoroughly +2. **FAQ**: Check the FAQ section above +3. **Troubleshooting**: Review the troubleshooting guide +4. **Search Issues**: Look for similar issues on GitHub +5. **Ask Questions**: Open a new issue with the "question" label + +### Reporting Issues + +When reporting bugs, please include: +- Operating system and version +- Python version (`python --version`) +- MKVToolNix version (`mkvmerge --version`) +- Complete error message +- Steps to reproduce +- Sample file info (if applicable) + +### Feature Requests + +We're always looking to improve! For feature requests: +- Check existing issues first to avoid duplicates +- Clearly describe the use case +- Explain why this would be valuable +- Consider contributing the feature yourself! + +### Contact + +- **GitHub Issues**: [https://github.com/mlbkumar9/Project_13/issues](https://github.com/mlbkumar9/Project_13/issues) +- **Discussions**: Use GitHub Discussions for questions and ideas +- **Security Issues**: See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities + +--- + +## πŸ™ Acknowledgments + +- **MKVToolNix Team**: For creating the excellent mkvmerge tool +- **Rich Library**: For beautiful terminal formatting ([https://github.com/Textualize/rich](https://github.com/Textualize/rich)) +- **Python Community**: For the robust standard library and ecosystem +- **Contributors**: Everyone who has contributed to this project + +--- + +## πŸ“š Additional Resources + +### Related Tools + +- **MKVToolNix**: [https://mkvtoolnix.download/](https://mkvtoolnix.download/) +- **FFmpeg**: [https://ffmpeg.org/](https://ffmpeg.org/) +- **HandBrake**: [https://handbrake.fr/](https://handbrake.fr/) +- **MediaInfo**: [https://mediaarea.net/en/MediaInfo](https://mediaarea.net/en/MediaInfo) + +### Learning Resources + +- **ISO 639-2 Standard**: [https://www.loc.gov/standards/iso639-2/](https://www.loc.gov/standards/iso639-2/) +- **Matroska Specification**: [https://www.matroska.org/technical/specs/index.html](https://www.matroska.org/technical/specs/index.html) +- **Python Documentation**: [https://docs.python.org/3/](https://docs.python.org/3/) + +### Project Links + +- **Repository**: [https://github.com/mlbkumar9/Project_13](https://github.com/mlbkumar9/Project_13) +- **Issues**: [https://github.com/mlbkumar9/Project_13/issues](https://github.com/mlbkumar9/Project_13/issues) +- **Changelog**: [CHANGELOG.md](CHANGELOG.md) +- **Security Policy**: [SECURITY.md](SECURITY.md) + +--- + +## 🎬 Conclusion + +MKV Batch Remuxer is designed to make video file management easier, whether you're managing a personal media library or processing professional content. With both GUI and CLI interfaces, comprehensive language support, and robust error handling, it provides a reliable solution for MKV file remuxing. + +**Key Takeaways**: +- βœ… Lossless quality preservation +- βœ… Easy language track selection +- βœ… Batch processing capability +- βœ… Cross-platform compatibility +- βœ… User-friendly interfaces +- βœ… Comprehensive logging and error handling + +**Getting Started**: +1. Install MKVToolNix and Python +2. Clone the repository +3. Install dependencies (`pip install -r requirements.txt`) +4. Run GUI (`python REMUX_GUI.py`) or CLI (`python REMUX_Script.py`) +5. Start processing your MKV files! + +For questions, issues, or contributions, please visit our [GitHub repository](https://github.com/mlbkumar9/Project_13). + +Happy remuxing! 🎬 + +--- + +*Last updated: 2025-10-18* +*Version: 1.0* -1. **Run the script:** - ```bash - python "REMUX Python Scripts/1_Improved_REMUX_Script.py" - ``` -2. **Follow the prompts:** - - The script will ask you to enter the path to your input directory. - - It will then ask for the output directory path (you can leave this blank to create a `remuxed` folder inside the input directory). - - You will be prompted to enter the desired audio and subtitle languages (comma-separated). - - Finally, you will be asked if you want to skip existing files or perform a dry run. - - The script will then display a live progress table in your terminal. diff --git a/REMUX Python Scripts/remux_gui.py b/REMUX Python Scripts/REMUX_GUI.py similarity index 65% rename from REMUX Python Scripts/remux_gui.py rename to REMUX Python Scripts/REMUX_GUI.py index 51e8b73..062ee26 100644 --- a/REMUX Python Scripts/remux_gui.py +++ b/REMUX Python Scripts/REMUX_GUI.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ remux_gui.py -A GUI for the batch remuxing script, built with tkinter. +A GUI for the batch remuxing script with automatic language detection, built with tkinter. """ import json @@ -36,6 +36,27 @@ def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): except Exception as e: return {"ok": False, "err": str(e)} +def get_available_languages(mkvmerge_path: str, files: list): + """Scan all files and return available audio and subtitle languages.""" + all_audio_langs = set() + all_sub_langs = set() + + for file in files: + ident = identify_tracks(mkvmerge_path, file) + if not ident["ok"]: + continue + + for track in ident["data"].get("tracks", []): + track_type = track.get("type", "").lower() + lang = track.get("properties", {}).get("language", "und").lower() + + if track_type == "audio": + all_audio_langs.add(lang) + elif track_type in ("subtitles", "subtitle"): + all_sub_langs.add(lang) + + return sorted(all_audio_langs), sorted(all_sub_langs) + def fmt_time(sec: Optional[float]) -> str: if sec is None: return "--:--" @@ -70,11 +91,11 @@ def send_update(data): audio_ids = [ t["id"] for t in ident["data"].get("tracks", []) - if t.get("type") == "audio" and (t.get("properties", {}).get("language") or "").lower() in audio_langs + if t.get("type") == "audio" and (t.get("properties", {}).get("language") or "und").lower() in audio_langs ] sub_ids = [ t["id"] for t in ident["data"].get("tracks", []) - if t.get("type") == "subtitles" and (t.get("properties", {}).get("language") or "").lower() in sub_langs + if t.get("type") in ("subtitles", "subtitle") and (t.get("properties", {}).get("language") or "und").lower() in sub_langs ] if not audio_ids: @@ -124,11 +145,13 @@ class RemuxApp: def __init__(self, root): self.root = root self.root.title("MKV Batch Remuxer") - self.root.geometry("900x600") + self.root.geometry("950x700") self.mkvmerge_path = find_mkvmerge() self.update_queue = queue.Queue() self.running = False + self.available_audio_langs = [] + self.available_sub_langs = [] # --- UI Setup --- main_frame = ttk.Frame(root, padding="10") @@ -149,25 +172,56 @@ def __init__(self, root): ttk.Entry(io_frame, textvariable=self.output_dir).grid(row=1, column=1, sticky=tk.EW) ttk.Button(io_frame, text="Browse...", command=self.browse_output).grid(row=1, column=2, padx=5) + # Available Languages Display Frame + avail_frame = ttk.LabelFrame(main_frame, text="Available Languages in MKV Files", padding="10") + avail_frame.pack(fill=tk.X, pady=5) + + # Audio languages display + ttk.Label(avail_frame, text="Audio:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2) + self.avail_audio_label = ttk.Label(avail_frame, text="Select input directory to scan...", + foreground="gray", relief=tk.SUNKEN, padding=5) + self.avail_audio_label.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=2) + + # Subtitle languages display + ttk.Label(avail_frame, text="Subtitles:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2) + self.avail_sub_label = ttk.Label(avail_frame, text="Select input directory to scan...", + foreground="gray", relief=tk.SUNKEN, padding=5) + self.avail_sub_label.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=2) + + avail_frame.columnconfigure(1, weight=1) + # Options Frame - opts_frame = ttk.LabelFrame(main_frame, text="Options", padding="10") + opts_frame = ttk.LabelFrame(main_frame, text="Language Selection", padding="10") opts_frame.pack(fill=tk.X, pady=5) opts_frame.columnconfigure(1, weight=1) opts_frame.columnconfigure(3, weight=1) ttk.Label(opts_frame, text="Audio Langs:").grid(row=0, column=0, sticky=tk.W, padx=5) self.audio_langs = tk.StringVar(value="eng") - ttk.Entry(opts_frame, textvariable=self.audio_langs).grid(row=0, column=1, sticky=tk.EW, padx=5) + audio_entry = ttk.Entry(opts_frame, textvariable=self.audio_langs) + audio_entry.grid(row=0, column=1, sticky=tk.EW, padx=5) + ttk.Label(opts_frame, text="(comma-separated, e.g., eng,jpn)", foreground="gray", font=("TkDefaultFont", 8)).grid(row=1, column=1, sticky=tk.W, padx=5) ttk.Label(opts_frame, text="Subtitle Langs:").grid(row=0, column=2, sticky=tk.W, padx=5) self.sub_langs = tk.StringVar(value="eng") - ttk.Entry(opts_frame, textvariable=self.sub_langs).grid(row=0, column=3, sticky=tk.EW, padx=5) + sub_entry = ttk.Entry(opts_frame, textvariable=self.sub_langs) + sub_entry.grid(row=0, column=3, sticky=tk.EW, padx=5) + ttk.Label(opts_frame, text="(comma-separated, e.g., eng,ger)", foreground="gray", font=("TkDefaultFont", 8)).grid(row=1, column=3, sticky=tk.W, padx=5) + + # Quick select buttons + btn_frame = ttk.Frame(opts_frame) + btn_frame.grid(row=2, column=0, columnspan=4, pady=10) + + ttk.Button(btn_frame, text="Select All Audio", command=self.select_all_audio).pack(side=tk.LEFT, padx=5) + ttk.Button(btn_frame, text="Select All Subtitles", command=self.select_all_subs).pack(side=tk.LEFT, padx=5) + ttk.Button(btn_frame, text="Clear Audio", command=lambda: self.audio_langs.set("")).pack(side=tk.LEFT, padx=5) + ttk.Button(btn_frame, text="Clear Subtitles", command=lambda: self.sub_langs.set("")).pack(side=tk.LEFT, padx=5) self.skip_exists = tk.BooleanVar(value=True) - ttk.Checkbutton(opts_frame, text="Skip if output exists", variable=self.skip_exists).grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) + ttk.Checkbutton(opts_frame, text="Skip if output exists", variable=self.skip_exists).grid(row=3, column=0, columnspan=2, sticky=tk.W, padx=5, pady=5) self.dry_run = tk.BooleanVar(value=False) - ttk.Checkbutton(opts_frame, text="Dry-run only", variable=self.dry_run).grid(row=1, column=2, columnspan=2, sticky=tk.W, padx=5, pady=5) + ttk.Checkbutton(opts_frame, text="Dry-run only", variable=self.dry_run).grid(row=3, column=2, columnspan=2, sticky=tk.W, padx=5, pady=5) # File List / Progress Table tree_frame = ttk.Frame(main_frame) @@ -198,7 +252,7 @@ def __init__(self, root): self.start_button = ttk.Button(control_frame, text="Start Remux", command=self.start_remux) self.start_button.pack(side=tk.RIGHT) - self.status_label = ttk.Label(control_frame, text="Ready.", anchor=tk.W) + self.status_label = ttk.Label(control_frame, text="Ready. Select an input directory to begin.", anchor=tk.W) self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True) if not self.mkvmerge_path: @@ -211,6 +265,8 @@ def browse_input(self): self.input_dir.set(path) if not self.output_dir.get(): self.output_dir.set(str(Path(path) / "remuxed")) + # Automatically scan for languages + self.scan_languages() def browse_output(self): path = filedialog.askdirectory() @@ -220,6 +276,65 @@ def browse_output(self): def show_error(self, message): messagebox.showerror("Error", message) + def scan_languages(self): + """Automatically scan MKV files and display available languages.""" + input_path = Path(self.input_dir.get()) + + if not input_path.is_dir(): + return + + mkv_files = sorted(input_path.glob("*.mkv")) + if not mkv_files: + self.avail_audio_label.config(text="No MKV files found", foreground="red") + self.avail_sub_label.config(text="No MKV files found", foreground="red") + self.status_label.config(text="No .mkv files found in the input directory.") + return + + self.avail_audio_label.config(text=f"Scanning {len(mkv_files)} files...", foreground="blue") + self.avail_sub_label.config(text=f"Scanning {len(mkv_files)} files...", foreground="blue") + self.status_label.config(text=f"Scanning {len(mkv_files)} files for available languages...") + self.root.update() + + # Run scan in separate thread to avoid freezing GUI + def scan_thread(): + audio_langs, sub_langs = get_available_languages(self.mkvmerge_path, mkv_files) + self.root.after(0, lambda: self.update_available_languages(audio_langs, sub_langs)) + + threading.Thread(target=scan_thread, daemon=True).start() + + def update_available_languages(self, audio_langs, sub_langs): + """Update the display with scanned languages.""" + self.available_audio_langs = audio_langs + self.available_sub_langs = sub_langs + + if audio_langs: + audio_text = ", ".join(audio_langs) + self.avail_audio_label.config(text=audio_text, foreground="green") + else: + self.avail_audio_label.config(text="None found", foreground="orange") + + if sub_langs: + sub_text = ", ".join(sub_langs) + self.avail_sub_label.config(text=sub_text, foreground="green") + else: + self.avail_sub_label.config(text="None found", foreground="orange") + + self.status_label.config(text=f"Scan complete. Found {len(audio_langs)} audio and {len(sub_langs)} subtitle languages.") + + def select_all_audio(self): + """Select all available audio languages.""" + if self.available_audio_langs: + self.audio_langs.set(",".join(self.available_audio_langs)) + else: + messagebox.showinfo("No Languages", "No audio languages available. Please scan files first.") + + def select_all_subs(self): + """Select all available subtitle languages.""" + if self.available_sub_langs: + self.sub_langs.set(",".join(self.available_sub_langs)) + else: + messagebox.showinfo("No Languages", "No subtitle languages available. Please scan files first.") + def start_remux(self): if self.running: return diff --git a/REMUX Python Scripts/1_Improved_REMUX_Script.py b/REMUX Python Scripts/REMUX_Script.py similarity index 83% rename from REMUX Python Scripts/1_Improved_REMUX_Script.py rename to REMUX Python Scripts/REMUX_Script.py index 5ad3d7d..bea64d0 100644 --- a/REMUX Python Scripts/1_Improved_REMUX_Script.py +++ b/REMUX Python Scripts/REMUX_Script.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ remux_rich_improved.py -Batch remux MKV files keeping only English audio + English subtitles. +Batch remux MKV files with user-selected audio and subtitle languages. Rich live table UI with improved layout: separate % column, persistent progress bars, separate result column, and gap before batch row. """ @@ -42,6 +42,61 @@ def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): except Exception as e: return {"ok": False, "err": str(e)} +def get_available_languages(mkvmerge_path: str, files: list): + """Scan all MKV files and return available audio and subtitle languages.""" + all_audio_langs = set() + all_sub_langs = set() + + console.print("\n[yellow]Scanning files for available languages...[/yellow]") + + for file in files: + ident = identify_tracks(mkvmerge_path, file) + if not ident["ok"]: + continue + + for track in ident["data"].get("tracks", []): + track_type = track.get("type", "").lower() + lang = track.get("properties", {}).get("language", "und").lower() + + if track_type == "audio": + all_audio_langs.add(lang) + elif track_type in ("subtitles", "subtitle"): + all_sub_langs.add(lang) + + return sorted(all_audio_langs), sorted(all_sub_langs) + +def prompt_language_selection(audio_langs_available, sub_langs_available): + """Prompt user to select languages from available options.""" + console.print(f"\n[bold cyan]Available Audio Languages:[/bold cyan] {', '.join(audio_langs_available) if audio_langs_available else 'None found'}") + console.print(f"[bold cyan]Available Subtitle Languages:[/bold cyan] {', '.join(sub_langs_available) if sub_langs_available else 'None found'}\n") + + # Audio language selection + audio_input = input("Enter audio languages to keep (comma-separated, e.g., 'eng,jpn') or press Enter for all: ").strip() + if audio_input: + audio_langs = [lang.strip().lower() for lang in audio_input.split(",") if lang.strip()] + # Validate user input + invalid_audio = [lang for lang in audio_langs if lang not in audio_langs_available] + if invalid_audio: + console.print(f"[yellow]Warning: These audio languages were not found: {', '.join(invalid_audio)}[/yellow]") + else: + audio_langs = list(audio_langs_available) + + # Subtitle language selection + sub_input = input("Enter subtitle languages to keep (comma-separated, e.g., 'eng,jpn') or press Enter for all: ").strip() + if sub_input: + sub_langs = [lang.strip().lower() for lang in sub_input.split(",") if lang.strip()] + # Validate user input + invalid_subs = [lang for lang in sub_langs if lang not in sub_langs_available] + if invalid_subs: + console.print(f"[yellow]Warning: These subtitle languages were not found: {', '.join(invalid_subs)}[/yellow]") + else: + sub_langs = list(sub_langs_available) + + console.print(f"\n[green]Selected audio languages:[/green] {', '.join(audio_langs) if audio_langs else 'None'}") + console.print(f"[green]Selected subtitle languages:[/green] {', '.join(sub_langs) if sub_langs else 'None'}\n") + + return audio_langs, sub_langs + def shorten(name: str, max_len: int = 40) -> str: if len(name) <= max_len: return name @@ -404,15 +459,6 @@ def main(): output_dir = Path(outp) if outp else input_dir / "remuxed" output_dir.mkdir(parents=True, exist_ok=True) - audio_langs_str = input("Enter desired audio languages (comma-separated, e.g., eng,jpn): ").strip().lower() - audio_langs = [lang.strip() for lang in audio_langs_str.split(',') if lang.strip()] - if not audio_langs: - console.print("[red]At least one audio language is required.[/]") - return - - sub_langs_str = input("Enter desired subtitle languages (comma-separated, e.g., eng,ger) (blank for none): ").strip().lower() - sub_langs = [lang.strip() for lang in sub_langs_str.split(',') if lang.strip()] - skip_if_exists = (input("Skip files if output exists? (Y/n) [Y]: ").strip().lower() != "n") dry_run = (input("Dry-run only? (y/N) [N]: ").strip().lower() == "y") @@ -430,6 +476,20 @@ def main(): console.print(f"Found {total} MKV file(s) in: {input_dir}\n") + # Scan files for available languages + audio_langs_available, sub_langs_available = get_available_languages(mkvmerge_path, mkv_files) + + if not audio_langs_available: + console.print("[red]No audio tracks found in any file. Cannot proceed.[/]") + return + + # Prompt user for language selection + audio_langs, sub_langs = prompt_language_selection(audio_langs_available, sub_langs_available) + + if not audio_langs: + console.print("[red]At least one audio language must be selected.[/]") + return + # prepare rows rows = [] for i, f in enumerate(mkv_files, start=1): @@ -449,7 +509,8 @@ def main(): log_path = output_dir / "remux_log.txt" with open(log_path, "a", encoding="utf-8") as lf: lf.write(f"==== remux run: {time.strftime('%Y-%m-%d %H:%M:%S')} ====\n") - lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n\n") + lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n") + lf.write(f"Audio languages: {', '.join(audio_langs)}\nSubtitle languages: {', '.join(sub_langs)}\n\n") # store executed commands for debug (optional) log_commands = [] @@ -502,4 +563,4 @@ def main(): console.print(f"\n[green]Completed: {ok_count} OK, {fail_count} failed. Log saved to:[/] {log_path}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/REMUX Python Scripts/improved-remux-script.py b/REMUX Python Scripts/improved-remux-script.py deleted file mode 100644 index c3902d3..0000000 --- a/REMUX Python Scripts/improved-remux-script.py +++ /dev/null @@ -1,491 +0,0 @@ -#!/usr/bin/env python3 -""" -remux_rich_improved.py -Batch remux MKV files keeping only English audio + English subtitles. -Rich live table UI with improved layout: separate % column, persistent progress bars, -separate result column, and gap before batch row. -""" - -import json -import queue -import shutil -import subprocess -import sys -import threading -import time -import re -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.table import Table -from rich.live import Live - -console = Console() - -# ---------- Helpers ---------- - -def find_mkvmerge() -> Optional[str]: - mk = shutil.which("mkvmerge") or shutil.which("mkvmerge.exe") - if mk: - return mk - candidate = r"C:\Program Files\MKVToolNix\mkvmerge.exe" - return candidate if Path(candidate).exists() else None - -def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): - cmd = [mkvmerge_path, "--identify", "--identification-format", "json", str(src)] - try: - res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, check=False) - if res.returncode != 0: - return {"ok": False, "err": res.stderr.strip() or res.stdout.strip() or f"rc={res.returncode}"} - return {"ok": True, "data": json.loads(res.stdout)} - except Exception as e: - return {"ok": False, "err": str(e)} - -def shorten(name: str, max_len: int = 40) -> str: - if len(name) <= max_len: - return name - half = (max_len - 3) // 2 - return name[:half] + "..." + name[-half:] - -def fmt_time(sec: Optional[float]) -> str: - if sec is None: - return "--:--" - try: - s = int(round(sec)) - m = s // 60 - ss = s % 60 - return f"{m:02d}:{ss:02d}" - except Exception: - return "--:--" - -def _start_stdout_reader(stream, q: queue.Queue): - def _reader(): - try: - for ln in iter(stream.readline, ""): - q.put(ln) - except Exception: - pass - finally: - try: - stream.close() - except Exception: - pass - t = threading.Thread(target=_reader, daemon=True) - t.start() - return t - -# ---------- Rendering ---------- - -def build_table(rows, batch_info): - """Return a rich.Table based on current rows and batch_info dict.""" - table = Table(show_header=True, header_style="bold magenta", expand=False) - table.add_column("No", width=4, justify="right") - table.add_column("Filename", width=40, overflow="fold") - table.add_column("Progress", width=22) - table.add_column("%", width=5, justify="right") - table.add_column("Elapsed", width=8, justify="right") - table.add_column("Remaining", width=10, justify="right") - table.add_column("Status", width=25) - table.add_column("Result", width=4, justify="center") - - for r in rows: - # filename (without checkmark/cross - those go in Result column) - fname_display = f"[cyan]{r['display_name']}[/cyan]" - - # progress bar - always show, regardless of completion status - pct = int(r.get("pct", 0)) - blocks = pct // 5 # 20 blocks total - bar = "β–ˆ" * blocks + "β–‘" * (20 - blocks) - progress_cell = f"[white]{bar}[/white]" - - # percentage column - pct_display = f"{pct:3d}%" - - elapsed = fmt_time(r.get("elapsed")) - remaining = fmt_time(r.get("remaining")) if r.get("remaining") is not None else "--:--" - status_text = r.get("status_text", "") - - # result column (checkmark or cross) - result = "" - if r.get("finished"): - if r["success"]: - result = "[green]βœ“[/green]" - else: - result = "[red]βœ—[/red]" - - table.add_row( - str(r["no"]), - fname_display, - progress_cell, - pct_display, - elapsed, - remaining, - status_text, - result - ) - - # Add an empty row for separation - table.add_row("", "", "", "", "", "", "", "") - - # Batch row - b = batch_info - batch_bar_blocks = int((b["pct"] // 5)) - batch_bar = "β–ˆ" * batch_bar_blocks + "β–‘" * (20 - batch_bar_blocks) - batch_progress_cell = f"[white]{batch_bar}[/white]" - batch_pct = f"{b['pct']:3d}%" - - batch_elapsed = fmt_time(b.get("elapsed")) - batch_remaining = fmt_time(b.get("remaining")) if b.get("remaining") is not None else "--:--" - batch_status = f"{b['done']}/{b['total']} done" - - batch_result = "" - if b["finished"]: - batch_result = "[green]βœ“[/green]" - - table.add_row( - "", - "[yellow]Batch progress[/yellow]", - batch_progress_cell, - batch_pct, - batch_elapsed, - batch_remaining, - batch_status, - batch_result - ) - - return table - -# ---------- Remux function (updates rows in place and refreshes live) ---------- - -def remux_file_and_update(mkvmerge_path: str, src_path: Path, out_path: Path, row_idx: int, - rows, live: Live, batch_start: float, completed_times: list, log_commands: list, - skip_if_exists: bool, dry_run: bool): - """ - Perform remux and update rows[row_idx] and call live.update(build_table(...)) frequently. - Returns True/False (success), elapsed_seconds, status_string (for logging). - """ - - # prepare row - row = rows[row_idx] - row["status_text"] = "Pending" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = None - row["finished"] = False - row["success"] = False - row["start_time"] = time.time() - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # skip if exists - if skip_if_exists and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "Skipped (exists)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "Skipped (exists)" - - # identify tracks - ident = identify_tracks(mkvmerge_path, src_path) - if not ident["ok"]: - row["finished"] = True - row["success"] = False - row["status_text"] = f"FAILED identify" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"identify failed: {ident.get('err')}" - - # pick audio & subtitle ids - audio_ids = [] - sub_ids = [] - for t in ident["data"].get("tracks", []): - ttype = (t.get("type") or "").lower() - props = t.get("properties") or {} - lang = (props.get("language") or "").lower() - tid = t.get("id") - if ttype == "audio" and isinstance(tid, int) and lang.startswith("en"): - audio_ids.append(tid) - if ttype in ("subtitles", "subtitle") and isinstance(tid, int) and lang.startswith("en"): - sub_ids.append(tid) - - if not audio_ids: - row["finished"] = True - row["success"] = False - row["status_text"] = "No English audio" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, "no English audio" - - # build mkvmerge command - cmd = [mkvmerge_path, "-o", str(out_path), "--audio-tracks", ",".join(map(str, audio_ids))] - if sub_ids: - cmd += ["--subtitle-tracks", ",".join(map(str, sub_ids))] - else: - cmd += ["--no-subtitles"] - cmd += ["--ui-language", "en", "--gui-mode", str(src_path)] - log_commands.append(" ".join(cmd)) - - if dry_run: - row["pct"] = 100 - row["finished"] = True - row["success"] = True - row["status_text"] = "Dry-run (skipped)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "dry-run" - - # spawn mkvmerge - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, encoding="utf-8", errors="replace") - except Exception as e: - row["finished"] = True - row["success"] = False - row["status_text"] = f"Launch failed" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"launch error: {e}" - - q = queue.Queue() - _start_stdout_reader(proc.stdout, q) - re_progress = re.compile(r"(?:#GUI#progress|Progress:)\s*([0-9]{1,3})") - src_size = src_path.stat().st_size if src_path.exists() else None - had_progress = False - full_lines = [] - start = time.time() - - # live update loop - while proc.poll() is None or not q.empty(): - try: - line = q.get(timeout=0.25) - except queue.Empty: - line = None - - if line is not None: - full_lines.append(line.rstrip("\n")) - m = re_progress.search(line) - if m: - pct = int(m.group(1)) - if pct < 0: - pct = 0 - if pct > 100: - pct = 100 - had_progress = True - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - # update batch row and table - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - else: - # fallback filesize based progress (if mkvmerge doesn't emit progress) - if (not had_progress) and out_path.exists() and src_size: - try: - cur = out_path.stat().st_size - pct = int(min(99, (cur / src_size) * 100)) - except Exception: - pct = 0 - if pct > row["pct"]: - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - rc = proc.wait() - elapsed = time.time() - start - tail = "\n".join(full_lines[-12:]) if full_lines else "" - if rc == 0 and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "OK" - completed_times.append(elapsed) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, elapsed, "OK" - else: - # Keep the progress as is, don't reset to 0 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = False - reason = f"rc={rc}" - if "Error" in tail: - row["status_text"] = "FAILED" - else: - row["status_text"] = f"FAILED (rc={rc})" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, elapsed, reason - -# ---------- Batch helper ---------- - -def compute_batch(rows, batch_start, completed_times): - total = len(rows) - done = sum(1 for r in rows if r.get("finished") and r.get("success")) - # batch pct - average of per-file pct - avg_pct = int(sum(r.get("pct", 0) for r in rows) / total) if total > 0 else 0 - elapsed = time.time() - batch_start - # estimate remaining: use avg of completed times if available; account for current partial progress - remaining = None - if len(completed_times) > 0: - avg_time = sum(completed_times) / len(completed_times) - remaining_files = total - sum(1 for r in rows if r.get("finished")) - # estimate for the currently running file's remaining (if any) - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - if current_remaining is None: - current_remaining = 0.0 - remaining = current_remaining + max(0, remaining_files - 1) * avg_time - else: - # no completed times yet -> try to estimate from current file only - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - remaining = current_remaining - return { - "pct": avg_pct, - "elapsed": elapsed, - "remaining": remaining, - "done": sum(1 for r in rows if r.get("finished")), - "total": total, - "finished": all(r.get("finished") for r in rows) - } - -# ---------- Main ---------- - -def main(): - console.print("[bold cyan]=== MKVToolNix batch remux (English only) ===[/]") - - inp = input("Enter input directory path: ").strip().strip('"') - if not inp: - console.print("[red]Input directory required.[/]") - return - input_dir = Path(inp) - if not input_dir.exists() or not input_dir.is_dir(): - console.print(f"[red]Directory not found: {input_dir}[/]") - return - - outp = input("Enter output directory path (blank = remuxed): ").strip().strip('"') - output_dir = Path(outp) if outp else input_dir / "remuxed" - output_dir.mkdir(parents=True, exist_ok=True) - skip_if_exists = (input("Skip files if output exists? (Y/n) [Y]: ").strip().lower() != "n") - dry_run = (input("Dry-run only? (y/N) [N]: ").strip().lower() == "y") - - mkvmerge_path = find_mkvmerge() - if not mkvmerge_path: - console.print("[red]βœ— mkvmerge not found on PATH. Install MKVToolNix or add mkvmerge to PATH.[/]") - return - - # collect mkv files - mkv_files = sorted(input_dir.glob("*.mkv")) - total = len(mkv_files) - if total == 0: - console.print("[yellow]No .mkv files found in the input directory.[/]") - return - - console.print(f"Found {total} MKV file(s) in: {input_dir}\n") - - # prepare rows - rows = [] - for i, f in enumerate(mkv_files, start=1): - rows.append({ - "no": i, - "fullname": f.name, - "display_name": shorten(f.name, 40), - "pct": 0, - "elapsed": 0.0, - "remaining": None, - "status_text": "Pending", - "finished": False, - "success": False, - "start_time": None - }) - - log_path = output_dir / "remux_log.txt" - with open(log_path, "a", encoding="utf-8") as lf: - lf.write(f"==== remux run: {time.strftime('%Y-%m-%d %H:%M:%S')} ====\n") - lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n\n") - - # store executed commands for debug (optional) - log_commands = [] - - # run live table - batch_start = time.time() - completed_times = [] - - with Live(build_table(rows, compute_batch(rows, batch_start, completed_times)), refresh_per_second=8, console=console) as live: - for idx, src in enumerate(mkv_files): - out_file = output_dir / src.name - ok, elapsed, reason = remux_file_and_update( - mkvmerge_path, src, out_file, idx, rows, live, batch_start, completed_times, log_commands, - skip_if_exists, dry_run - ) - # status already updated inside function; ensure last render - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # write final plain-text summary table to log - final_lines = [] - final_lines.append("No\tFilename\tProgress\tElapsed\tRemaining\tStatus\tResult") - ok_count = 0 - fail_count = 0 - for r in rows: - status_text = r.get("status_text", "") - result_text = "" - if r.get("success"): - ok_count += 1 - result_text = "OK" - elif r.get("finished") and not r.get("success"): - fail_count += 1 - result_text = "FAILED" - final_lines.append( - f"{r['no']}\t{r['fullname']}\t{int(r.get('pct',0))}%\t" - f"{fmt_time(r.get('elapsed'))}\t{fmt_time(r.get('remaining')) if r.get('remaining') is not None else '--:--'}\t" - f"{status_text}\t{result_text}" - ) - - total_elapsed = time.time() - batch_start - - with open(log_path, "a", encoding="utf-8") as lf: - lf.write("\n".join(final_lines) + "\n\n") - lf.write(f"=== Summary ===\nProcessed: {total}, OK: {ok_count}, Failed: {fail_count}\nTotal time: {fmt_time(total_elapsed)}\n") - if log_commands: - lf.write("\nCommands executed (sample):\n") - for c in log_commands[:10]: - lf.write(c + "\n") - lf.write("\n") - - console.print(f"\n[green]Completed: {ok_count} OK, {fail_count} failed. Log saved to:[/] {log_path}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/REMUX Python Scripts/improved_remux_script_2.py b/REMUX Python Scripts/improved_remux_script_2.py deleted file mode 100644 index 267cb12..0000000 --- a/REMUX Python Scripts/improved_remux_script_2.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -""" -remux_rich_improved.py -Batch remux MKV files keeping only English audio + English subtitles. -Rich live table UI with improved layout: separate % column, persistent progress bars, -separate result column, and gap before batch row. -""" - -import json -import queue -import shutil -import subprocess -import sys -import threading -import time -import re -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.table import Table -from rich.live import Live - -console = Console() - -# ---------- Helpers ---------- - -def find_mkvmerge() -> Optional[str]: - mk = shutil.which("mkvmerge") or shutil.which("mkvmerge.exe") - if mk: - return mk - candidate = r"C:\Program Files\MKVToolNix\mkvmerge.exe" - return candidate if Path(candidate).exists() else None - -def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): - cmd = [mkvmerge_path, "--identify", "--identification-format", "json", str(src)] - try: - res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, check=False) - if res.returncode != 0: - return {"ok": False, "err": res.stderr.strip() or res.stdout.strip() or f"rc={res.returncode}"} - return {"ok": True, "data": json.loads(res.stdout)} - except Exception as e: - return {"ok": False, "err": str(e)} - -def shorten(name: str, max_len: int = 40) -> str: - if len(name) <= max_len: - return name - half = (max_len - 3) // 2 - return name[:half] + "..." + name[-half:] - -def fmt_time(sec: Optional[float]) -> str: - if sec is None: - return "--:--" - try: - s = int(round(sec)) - m = s // 60 - ss = s % 60 - return f"{m:02d}:{ss:02d}" - except Exception: - return "--:--" - -def _start_stdout_reader(stream, q: queue.Queue): - def _reader(): - try: - for ln in iter(stream.readline, ""): - q.put(ln) - except Exception: - pass - finally: - try: - stream.close() - except Exception: - pass - t = threading.Thread(target=_reader, daemon=True) - t.start() - return t - -# ---------- Rendering ---------- - -def build_table(rows, batch_info): - """Return a rich.Table based on current rows and batch_info dict.""" - table = Table(show_header=True, header_style="bold magenta", expand=False) - table.add_column("SL No.", width=6, justify="right") - table.add_column("Filename", width=40, overflow="fold") - table.add_column("Progress", width=22) - table.add_column("%", width=5, justify="right") - table.add_column("Elapsed", width=8, justify="right") - table.add_column("Remaining", width=10, justify="right") - table.add_column("Status", width=12) - table.add_column("Result", width=8, justify="center") - - for i, r in enumerate(rows): - # filename (without checkmark/cross - those go in Result column) - fname_display = f"[cyan]{r['display_name']}[/cyan]" - - # progress bar - always show, regardless of completion status - pct = int(r.get("pct", 0)) - blocks = pct // 5 # 20 blocks total - bar = "β–ˆ" * blocks + "β–‘" * (20 - blocks) - progress_cell = f"[white]{bar}[/white]" - - # percentage column - green color - pct_display = f"[green]{pct:3d}%[/green]" - - elapsed = fmt_time(r.get("elapsed")) - remaining = fmt_time(r.get("remaining")) if r.get("remaining") is not None else "--:--" - status_text = r.get("status_text", "") - - # result column (checkmark or cross) - result = "" - if r.get("finished"): - if r["success"]: - result = "[green]βœ“[/green]" - else: - result = "[red]βœ—[/red]" - - table.add_row( - str(r["no"]), - fname_display, - progress_cell, - pct_display, - elapsed, - remaining, - status_text, - result - ) - - # Add separator row after each file (except the last one) - if i < len(rows) - 1: - table.add_row("", "", "", "", "", "", "", "") - - # Add an empty row for separation before batch row - table.add_row("", "", "", "", "", "", "", "") - - # Batch row - b = batch_info - batch_bar_blocks = int((b["pct"] // 5)) - batch_bar = "β–ˆ" * batch_bar_blocks + "β–‘" * (20 - batch_bar_blocks) - batch_progress_cell = f"[white]{batch_bar}[/white]" - batch_pct = f"[green]{b['pct']:3d}%[/green]" - - batch_elapsed = fmt_time(b.get("elapsed")) - batch_remaining = fmt_time(b.get("remaining")) if b.get("remaining") is not None else "--:--" - batch_status = f"{b['done']}/{b['total']} done" - - batch_result = "" - if b["finished"]: - batch_result = "[green]βœ“[/green]" - - table.add_row( - "", - "[yellow]Batch progress[/yellow]", - batch_progress_cell, - batch_pct, - batch_elapsed, - batch_remaining, - batch_status, - batch_result - ) - - return table - -# ---------- Remux function (updates rows in place and refreshes live) ---------- - -def remux_file_and_update(mkvmerge_path: str, src_path: Path, out_path: Path, row_idx: int, - rows, live: Live, batch_start: float, completed_times: list, log_commands: list, - skip_if_exists: bool, dry_run: bool): - """ - Perform remux and update rows[row_idx] and call live.update(build_table(...)) frequently. - Returns True/False (success), elapsed_seconds, status_string (for logging). - """ - - # prepare row - row = rows[row_idx] - row["status_text"] = "Pending" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = None - row["finished"] = False - row["success"] = False - row["start_time"] = time.time() - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # skip if exists - if skip_if_exists and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "Skipped (exists)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "Skipped (exists)" - - # identify tracks - ident = identify_tracks(mkvmerge_path, src_path) - if not ident["ok"]: - row["finished"] = True - row["success"] = False - row["status_text"] = f"FAILED identify" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"identify failed: {ident.get('err')}" - - # pick audio & subtitle ids - audio_ids = [] - sub_ids = [] - for t in ident["data"].get("tracks", []): - ttype = (t.get("type") or "").lower() - props = t.get("properties") or {} - lang = (props.get("language") or "").lower() - tid = t.get("id") - if ttype == "audio" and isinstance(tid, int) and lang.startswith("en"): - audio_ids.append(tid) - if ttype in ("subtitles", "subtitle") and isinstance(tid, int) and lang.startswith("en"): - sub_ids.append(tid) - - if not audio_ids: - row["finished"] = True - row["success"] = False - row["status_text"] = "No English audio" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, "no English audio" - - # build mkvmerge command - cmd = [mkvmerge_path, "-o", str(out_path), "--audio-tracks", ",".join(map(str, audio_ids))] - if sub_ids: - cmd += ["--subtitle-tracks", ",".join(map(str, sub_ids))] - else: - cmd += ["--no-subtitles"] - cmd += ["--ui-language", "en", "--gui-mode", str(src_path)] - log_commands.append(" ".join(cmd)) - - if dry_run: - row["pct"] = 100 - row["finished"] = True - row["success"] = True - row["status_text"] = "Dry-run (skipped)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "dry-run" - - # spawn mkvmerge - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, encoding="utf-8", errors="replace") - except Exception as e: - row["finished"] = True - row["success"] = False - row["status_text"] = f"Launch failed" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"launch error: {e}" - - q = queue.Queue() - _start_stdout_reader(proc.stdout, q) - re_progress = re.compile(r"(?:#GUI#progress|Progress:)\s*([0-9]{1,3})") - src_size = src_path.stat().st_size if src_path.exists() else None - had_progress = False - full_lines = [] - start = time.time() - - # live update loop - while proc.poll() is None or not q.empty(): - try: - line = q.get(timeout=0.25) - except queue.Empty: - line = None - - if line is not None: - full_lines.append(line.rstrip("\n")) - m = re_progress.search(line) - if m: - pct = int(m.group(1)) - if pct < 0: - pct = 0 - if pct > 100: - pct = 100 - had_progress = True - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - # update batch row and table - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - else: - # fallback filesize based progress (if mkvmerge doesn't emit progress) - if (not had_progress) and out_path.exists() and src_size: - try: - cur = out_path.stat().st_size - pct = int(min(99, (cur / src_size) * 100)) - except Exception: - pct = 0 - if pct > row["pct"]: - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - rc = proc.wait() - elapsed = time.time() - start - tail = "\n".join(full_lines[-12:]) if full_lines else "" - if rc == 0 and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "OK" - completed_times.append(elapsed) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, elapsed, "OK" - else: - # Keep the progress as is, don't reset to 0 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = False - reason = f"rc={rc}" - if "Error" in tail: - row["status_text"] = "FAILED" - else: - row["status_text"] = f"FAILED (rc={rc})" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, elapsed, reason - -# ---------- Batch helper ---------- - -def compute_batch(rows, batch_start, completed_times): - total = len(rows) - done = sum(1 for r in rows if r.get("finished") and r.get("success")) - # batch pct - average of per-file pct - avg_pct = int(sum(r.get("pct", 0) for r in rows) / total) if total > 0 else 0 - elapsed = time.time() - batch_start - # estimate remaining: use avg of completed times if available; account for current partial progress - remaining = None - if len(completed_times) > 0: - avg_time = sum(completed_times) / len(completed_times) - remaining_files = total - sum(1 for r in rows if r.get("finished")) - # estimate for the currently running file's remaining (if any) - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - if current_remaining is None: - current_remaining = 0.0 - remaining = current_remaining + max(0, remaining_files - 1) * avg_time - else: - # no completed times yet -> try to estimate from current file only - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - remaining = current_remaining - return { - "pct": avg_pct, - "elapsed": elapsed, - "remaining": remaining, - "done": sum(1 for r in rows if r.get("finished")), - "total": total, - "finished": all(r.get("finished") for r in rows) - } - -# ---------- Main ---------- - -def main(): - console.print("[bold cyan]=== MKVToolNix batch remux (English only) ===[/]") - - inp = input("Enter input directory path: ").strip().strip('"') - if not inp: - console.print("[red]Input directory required.[/]") - return - input_dir = Path(inp) - if not input_dir.exists() or not input_dir.is_dir(): - console.print(f"[red]Directory not found: {input_dir}[/]") - return - - outp = input("Enter output directory path (blank = remuxed): ").strip().strip('"') - output_dir = Path(outp) if outp else input_dir / "remuxed" - output_dir.mkdir(parents=True, exist_ok=True) - skip_if_exists = (input("Skip files if output exists? (Y/n) [Y]: ").strip().lower() != "n") - dry_run = (input("Dry-run only? (y/N) [N]: ").strip().lower() == "y") - - mkvmerge_path = find_mkvmerge() - if not mkvmerge_path: - console.print("[red]βœ— mkvmerge not found on PATH. Install MKVToolNix or add mkvmerge to PATH.[/]") - return - - # collect mkv files - mkv_files = sorted(input_dir.glob("*.mkv")) - total = len(mkv_files) - if total == 0: - console.print("[yellow]No .mkv files found in the input directory.[/]") - return - - console.print(f"Found {total} MKV file(s) in: {input_dir}\n") - - # prepare rows - rows = [] - for i, f in enumerate(mkv_files, start=1): - rows.append({ - "no": i, - "fullname": f.name, - "display_name": shorten(f.name, 40), - "pct": 0, - "elapsed": 0.0, - "remaining": None, - "status_text": "Pending", - "finished": False, - "success": False, - "start_time": None - }) - - log_path = output_dir / "remux_log.txt" - with open(log_path, "a", encoding="utf-8") as lf: - lf.write(f"==== remux run: {time.strftime('%Y-%m-%d %H:%M:%S')} ====\n") - lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n\n") - - # store executed commands for debug (optional) - log_commands = [] - - # run live table - batch_start = time.time() - completed_times = [] - - with Live(build_table(rows, compute_batch(rows, batch_start, completed_times)), refresh_per_second=8, console=console) as live: - for idx, src in enumerate(mkv_files): - out_file = output_dir / src.name - ok, elapsed, reason = remux_file_and_update( - mkvmerge_path, src, out_file, idx, rows, live, batch_start, completed_times, log_commands, - skip_if_exists, dry_run - ) - # status already updated inside function; ensure last render - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # write final plain-text summary table to log - final_lines = [] - final_lines.append("SL No.\tFilename\tProgress\tElapsed\tRemaining\tStatus\tResult") - ok_count = 0 - fail_count = 0 - for r in rows: - status_text = r.get("status_text", "") - result_text = "" - if r.get("success"): - ok_count += 1 - result_text = "OK" - elif r.get("finished") and not r.get("success"): - fail_count += 1 - result_text = "FAILED" - final_lines.append( - f"{r['no']}\t{r['fullname']}\t{int(r.get('pct',0))}%\t" - f"{fmt_time(r.get('elapsed'))}\t{fmt_time(r.get('remaining')) if r.get('remaining') is not None else '--:--'}\t" - f"{status_text}\t{result_text}" - ) - - total_elapsed = time.time() - batch_start - - with open(log_path, "a", encoding="utf-8") as lf: - lf.write("\n".join(final_lines) + "\n\n") - lf.write(f"=== Summary ===\nProcessed: {total}, OK: {ok_count}, Failed: {fail_count}\nTotal time: {fmt_time(total_elapsed)}\n") - if log_commands: - lf.write("\nCommands executed (sample):\n") - for c in log_commands[:10]: - lf.write(c + "\n") - lf.write("\n") - - console.print(f"\n[green]Completed: {ok_count} OK, {fail_count} failed. Log saved to:[/] {log_path}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/REMUX Python Scripts/improved_remux_script_3 - Original.py b/REMUX Python Scripts/improved_remux_script_3 - Original.py deleted file mode 100644 index 8d3bc3e..0000000 --- a/REMUX Python Scripts/improved_remux_script_3 - Original.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -""" -remux_rich_improved.py -Batch remux MKV files keeping only English audio + English subtitles. -Rich live table UI with improved layout: separate % column, persistent progress bars, -separate result column, and gap before batch row. -""" - -import json -import queue -import shutil -import subprocess -import sys -import threading -import time -import re -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.table import Table -from rich.live import Live - -console = Console() - -# ---------- Helpers ---------- - -def find_mkvmerge() -> Optional[str]: - mk = shutil.which("mkvmerge") or shutil.which("mkvmerge.exe") - if mk: - return mk - candidate = r"C:\Program Files\MKVToolNix\mkvmerge.exe" - return candidate if Path(candidate).exists() else None - -def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): - cmd = [mkvmerge_path, "--identify", "--identification-format", "json", str(src)] - try: - res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, check=False) - if res.returncode != 0: - return {"ok": False, "err": res.stderr.strip() or res.stdout.strip() or f"rc={res.returncode}"} - return {"ok": True, "data": json.loads(res.stdout)} - except Exception as e: - return {"ok": False, "err": str(e)} - -def shorten(name: str, max_len: int = 40) -> str: - if len(name) <= max_len: - return name - half = (max_len - 3) // 2 - return name[:half] + "..." + name[-half:] - -def fmt_time(sec: Optional[float]) -> str: - if sec is None: - return "--:--" - try: - s = int(round(sec)) - m = s // 60 - ss = s % 60 - return f"{m:02d}:{ss:02d}" - except Exception: - return "--:--" - -def _start_stdout_reader(stream, q: queue.Queue): - def _reader(): - try: - for ln in iter(stream.readline, ""): - q.put(ln) - except Exception: - pass - finally: - try: - stream.close() - except Exception: - pass - t = threading.Thread(target=_reader, daemon=True) - t.start() - return t - -# ---------- Rendering ---------- - -def build_table(rows, batch_info): - """Return a rich.Table based on current rows and batch_info dict.""" - table = Table(show_header=True, header_style="bold magenta", expand=False) - table.add_column("SL No.", width=6, justify="right") - table.add_column("Filename", width=40, overflow="fold") - table.add_column("Progress", width=22) - table.add_column("%", width=5, justify="right") - table.add_column("Elapsed", width=8, justify="right") - table.add_column("Remaining", width=10, justify="right") - table.add_column("Status", width=12) - table.add_column("Result", width=8, justify="center") - - for i, r in enumerate(rows): - # filename (without checkmark/cross - those go in Result column) - fname_display = f"[cyan]{r['display_name']}[/cyan]" - - # progress bar - always show, regardless of completion status - pct = int(r.get("pct", 0)) - blocks = pct // 5 # 20 blocks total - bar = "β–ˆ" * blocks + "β–‘" * (20 - blocks) - progress_cell = f"[white]{bar}[/white]" - - # percentage column - green color - pct_display = f"[green]{pct:3d}%[/green]" - - elapsed = fmt_time(r.get("elapsed")) - remaining = fmt_time(r.get("remaining")) if r.get("remaining") is not None else "--:--" - status_text = r.get("status_text", "") - - # result column (checkmark or cross) - result = "" - if r.get("finished"): - if r["success"]: - result = "[green]βœ“[/green]" - else: - result = "[red]βœ—[/red]" - - table.add_row( - str(r["no"]), - fname_display, - progress_cell, - pct_display, - elapsed, - remaining, - status_text, - result - ) - - # Add separator row after each file (except the last one) - if i < len(rows) - 1: - table.add_row("", "", "", "", "", "", "", "") - - # Add an empty row for separation before batch row - table.add_row("", "", "", "", "", "", "", "") - - # Batch row - b = batch_info - batch_bar_blocks = int((b["pct"] // 5)) - batch_bar = "β–ˆ" * batch_bar_blocks + "β–‘" * (20 - batch_bar_blocks) - batch_progress_cell = f"[white]{batch_bar}[/white]" - batch_pct = f"[green]{b['pct']:3d}%[/green]" - - batch_elapsed = fmt_time(b.get("elapsed")) - batch_remaining = fmt_time(b.get("remaining")) if b.get("remaining") is not None else "--:--" - batch_status = f"{b['done']}/{b['total']} done" - - batch_result = "" - if b["finished"]: - batch_result = "[green]βœ“[/green]" - - table.add_row( - "", - "[bold bright_yellow]Batch Progress[/bold bright_yellow]", - batch_progress_cell, - batch_pct, - batch_elapsed, - batch_remaining, - batch_status, - batch_result - ) - - return table - -# ---------- Remux function (updates rows in place and refreshes live) ---------- - -def remux_file_and_update(mkvmerge_path: str, src_path: Path, out_path: Path, row_idx: int, - rows, live: Live, batch_start: float, completed_times: list, log_commands: list, - skip_if_exists: bool, dry_run: bool): - """ - Perform remux and update rows[row_idx] and call live.update(build_table(...)) frequently. - Returns True/False (success), elapsed_seconds, status_string (for logging). - """ - - # prepare row - row = rows[row_idx] - row["status_text"] = "Pending" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = None - row["finished"] = False - row["success"] = False - row["start_time"] = time.time() - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # skip if exists - if skip_if_exists and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "Skipped (exists)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "Skipped (exists)" - - # identify tracks - ident = identify_tracks(mkvmerge_path, src_path) - if not ident["ok"]: - row["finished"] = True - row["success"] = False - row["status_text"] = f"FAILED identify" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"identify failed: {ident.get('err')}" - - # pick audio & subtitle ids - audio_ids = [] - sub_ids = [] - for t in ident["data"].get("tracks", []): - ttype = (t.get("type") or "").lower() - props = t.get("properties") or {} - lang = (props.get("language") or "").lower() - tid = t.get("id") - if ttype == "audio" and isinstance(tid, int) and lang.startswith("en"): - audio_ids.append(tid) - if ttype in ("subtitles", "subtitle") and isinstance(tid, int) and lang.startswith("en"): - sub_ids.append(tid) - - if not audio_ids: - row["finished"] = True - row["success"] = False - row["status_text"] = "No English audio" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, "no English audio" - - # build mkvmerge command - cmd = [mkvmerge_path, "-o", str(out_path), "--audio-tracks", ",".join(map(str, audio_ids))] - if sub_ids: - cmd += ["--subtitle-tracks", ",".join(map(str, sub_ids))] - else: - cmd += ["--no-subtitles"] - cmd += ["--ui-language", "en", "--gui-mode", str(src_path)] - log_commands.append(" ".join(cmd)) - - if dry_run: - row["pct"] = 100 - row["finished"] = True - row["success"] = True - row["status_text"] = "Dry-run (skipped)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "dry-run" - - # spawn mkvmerge - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, encoding="utf-8", errors="replace") - except Exception as e: - row["finished"] = True - row["success"] = False - row["status_text"] = f"Launch failed" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"launch error: {e}" - - q = queue.Queue() - _start_stdout_reader(proc.stdout, q) - re_progress = re.compile(r"(?:#GUI#progress|Progress:)\s*([0-9]{1,3})") - src_size = src_path.stat().st_size if src_path.exists() else None - had_progress = False - full_lines = [] - start = time.time() - - # live update loop - while proc.poll() is None or not q.empty(): - try: - line = q.get(timeout=0.25) - except queue.Empty: - line = None - - if line is not None: - full_lines.append(line.rstrip("\n")) - m = re_progress.search(line) - if m: - pct = int(m.group(1)) - if pct < 0: - pct = 0 - if pct > 100: - pct = 100 - had_progress = True - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - # update batch row and table - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - else: - # fallback filesize based progress (if mkvmerge doesn't emit progress) - if (not had_progress) and out_path.exists() and src_size: - try: - cur = out_path.stat().st_size - pct = int(min(99, (cur / src_size) * 100)) - except Exception: - pct = 0 - if pct > row["pct"]: - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - row["status_text"] = "Processing" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - rc = proc.wait() - elapsed = time.time() - start - tail = "\n".join(full_lines[-12:]) if full_lines else "" - if rc == 0 and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "OK" - completed_times.append(elapsed) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, elapsed, "OK" - else: - # Keep the progress as is, don't reset to 0 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = False - reason = f"rc={rc}" - if "Error" in tail: - row["status_text"] = "FAILED" - else: - row["status_text"] = f"FAILED (rc={rc})" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, elapsed, reason - -# ---------- Batch helper ---------- - -def compute_batch(rows, batch_start, completed_times): - total = len(rows) - done = sum(1 for r in rows if r.get("finished") and r.get("success")) - # batch pct - average of per-file pct - avg_pct = int(sum(r.get("pct", 0) for r in rows) / total) if total > 0 else 0 - elapsed = time.time() - batch_start - # estimate remaining: use avg of completed times if available; account for current partial progress - remaining = None - if len(completed_times) > 0: - avg_time = sum(completed_times) / len(completed_times) - remaining_files = total - sum(1 for r in rows if r.get("finished")) - # estimate for the currently running file's remaining (if any) - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - if current_remaining is None: - current_remaining = 0.0 - remaining = current_remaining + max(0, remaining_files - 1) * avg_time - else: - # no completed times yet -> try to estimate from current file only - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - remaining = current_remaining - return { - "pct": avg_pct, - "elapsed": elapsed, - "remaining": remaining, - "done": sum(1 for r in rows if r.get("finished")), - "total": total, - "finished": all(r.get("finished") for r in rows) - } - -# ---------- Main ---------- - -def main(): - console.print("[bold cyan]=== MKVToolNix batch remux (English only) ===[/]") - - inp = input("Enter input directory path: ").strip().strip('"') - if not inp: - console.print("[red]Input directory required.[/]") - return - input_dir = Path(inp) - if not input_dir.exists() or not input_dir.is_dir(): - console.print(f"[red]Directory not found: {input_dir}[/]") - return - - outp = input("Enter output directory path (blank = remuxed): ").strip().strip('"') - output_dir = Path(outp) if outp else input_dir / "remuxed" - output_dir.mkdir(parents=True, exist_ok=True) - skip_if_exists = (input("Skip files if output exists? (Y/n) [Y]: ").strip().lower() != "n") - dry_run = (input("Dry-run only? (y/N) [N]: ").strip().lower() == "y") - - mkvmerge_path = find_mkvmerge() - if not mkvmerge_path: - console.print("[red]βœ— mkvmerge not found on PATH. Install MKVToolNix or add mkvmerge to PATH.[/]") - return - - # collect mkv files - mkv_files = sorted(input_dir.glob("*.mkv")) - total = len(mkv_files) - if total == 0: - console.print("[yellow]No .mkv files found in the input directory.[/]") - return - - console.print(f"Found {total} MKV file(s) in: {input_dir}\n") - - # prepare rows - rows = [] - for i, f in enumerate(mkv_files, start=1): - rows.append({ - "no": i, - "fullname": f.name, - "display_name": shorten(f.name, 40), - "pct": 0, - "elapsed": 0.0, - "remaining": None, - "status_text": "Pending", - "finished": False, - "success": False, - "start_time": None - }) - - log_path = output_dir / "remux_log.txt" - with open(log_path, "a", encoding="utf-8") as lf: - lf.write(f"==== remux run: {time.strftime('%Y-%m-%d %H:%M:%S')} ====\n") - lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n\n") - - # store executed commands for debug (optional) - log_commands = [] - - # run live table - batch_start = time.time() - completed_times = [] - - with Live(build_table(rows, compute_batch(rows, batch_start, completed_times)), refresh_per_second=8, console=console) as live: - for idx, src in enumerate(mkv_files): - out_file = output_dir / src.name - ok, elapsed, reason = remux_file_and_update( - mkvmerge_path, src, out_file, idx, rows, live, batch_start, completed_times, log_commands, - skip_if_exists, dry_run - ) - # status already updated inside function; ensure last render - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # write final plain-text summary table to log - final_lines = [] - final_lines.append("SL No.\tFilename\tProgress\tElapsed\tRemaining\tStatus\tResult") - ok_count = 0 - fail_count = 0 - for r in rows: - status_text = r.get("status_text", "") - result_text = "" - if r.get("success"): - ok_count += 1 - result_text = "OK" - elif r.get("finished") and not r.get("success"): - fail_count += 1 - result_text = "FAILED" - final_lines.append( - f"{r['no']}\t{r['fullname']}\t{int(r.get('pct',0))}%\t" - f"{fmt_time(r.get('elapsed'))}\t{fmt_time(r.get('remaining')) if r.get('remaining') is not None else '--:--'}\t" - f"{status_text}\t{result_text}" - ) - - total_elapsed = time.time() - batch_start - - with open(log_path, "a", encoding="utf-8") as lf: - lf.write("\n".join(final_lines) + "\n\n") - lf.write(f"=== Summary ===\nProcessed: {total}, OK: {ok_count}, Failed: {fail_count}\nTotal time: {fmt_time(total_elapsed)}\n") - if log_commands: - lf.write("\nCommands executed (sample):\n") - for c in log_commands[:10]: - lf.write(c + "\n") - lf.write("\n") - - console.print(f"\n[green]Completed: {ok_count} OK, {fail_count} failed. Log saved to:[/] {log_path}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/REMUX Python Scripts/remux_me.py b/REMUX Python Scripts/remux_me.py deleted file mode 100644 index 8c07f2a..0000000 --- a/REMUX Python Scripts/remux_me.py +++ /dev/null @@ -1,447 +0,0 @@ -#!/usr/bin/env python3 -""" -remux_rich_final.py -Batch remux MKV files keeping only English audio + English subtitles. -Rich live table UI: all rows pre-created, white progress bars, elapsed+remaining columns, -status, and final plain-text summary log (remux_log.txt). -""" - -import json -import queue -import shutil -import subprocess -import sys -import threading -import time -import re -from pathlib import Path -from typing import Optional - -from rich.console import Console -from rich.table import Table -from rich.live import Live - -console = Console() - -# ---------- Helpers ---------- - -def find_mkvmerge() -> Optional[str]: - mk = shutil.which("mkvmerge") or shutil.which("mkvmerge.exe") - if mk: - return mk - candidate = r"C:\Program Files\MKVToolNix\mkvmerge.exe" - return candidate if Path(candidate).exists() else None - -def identify_tracks(mkvmerge_path: str, src: Path, timeout: float = 10.0): - cmd = [mkvmerge_path, "--identify", "--identification-format", "json", str(src)] - try: - res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=timeout, check=False) - if res.returncode != 0: - return {"ok": False, "err": res.stderr.strip() or res.stdout.strip() or f"rc={res.returncode}"} - return {"ok": True, "data": json.loads(res.stdout)} - except Exception as e: - return {"ok": False, "err": str(e)} - -def shorten(name: str, max_len: int = 40) -> str: - if len(name) <= max_len: - return name - half = (max_len - 3) // 2 - return name[:half] + "..." + name[-half:] - -def fmt_time(sec: Optional[float]) -> str: - if sec is None: - return "--:--" - try: - s = int(round(sec)) - m = s // 60 - ss = s % 60 - return f"{m:02d}:{ss:02d}" - except Exception: - return "--:--" - -def _start_stdout_reader(stream, q: queue.Queue): - def _reader(): - try: - for ln in iter(stream.readline, ""): - q.put(ln) - except Exception: - pass - finally: - try: - stream.close() - except Exception: - pass - t = threading.Thread(target=_reader, daemon=True) - t.start() - return t - -# ---------- Rendering ---------- - -def build_table(rows, batch_info): - """Return a rich.Table based on current rows and batch_info dict.""" - table = Table(show_header=True, header_style="bold magenta", expand=False) - table.add_column("No", width=4, justify="right") - table.add_column("Filename", width=40, overflow="fold") - table.add_column("Progress", width=30) - table.add_column("Elapsed", width=8, justify="right") - table.add_column("Remaining", width=10, justify="right") - table.add_column("Status", width=25) - - for r in rows: - # filename with check/cross appended (colored) - fname = r["display_name"] - if r.get("finished"): - if r["success"]: - fname_display = f"[cyan]{fname}[/cyan] [green]βœ…[/green]" - else: - fname_display = f"[cyan]{fname}[/cyan] [red]❌[/red]" - else: - fname_display = f"[cyan]{fname}[/cyan]" - - # progress bar or final label - pct = int(r.get("pct", 0)) - if r.get("finished"): - # show a compact finished label in Progress column - progress_cell = "[green]100%[/green]" if r["success"] else "[red]0%[/red]" - else: - blocks = pct // 5 # 20 blocks total - bar = "β–ˆ" * blocks + "β–‘" * (20 - blocks) - progress_cell = f"[white]{bar} {pct:3d}%[/white]" - - elapsed = fmt_time(r.get("elapsed")) - remaining = fmt_time(r.get("remaining")) if r.get("remaining") is not None else "--:--" - status_text = r.get("status_text", "") - - table.add_row(str(r["no"]), fname_display, progress_cell, elapsed, remaining, status_text) - - # Batch row appended as the last row - b = batch_info - batch_bar_blocks = int((b["pct"] // 5)) - batch_bar = "β–ˆ" * batch_bar_blocks + "β–‘" * (20 - batch_bar_blocks) - if b["finished"]: - batch_progress_cell = f"[green]βœ… 100% Completed[/green]" - else: - batch_progress_cell = f"[white]{batch_bar} {b['pct']:3d}%[/white]" - - batch_elapsed = fmt_time(b.get("elapsed")) - batch_remaining = fmt_time(b.get("remaining")) if b.get("remaining") is not None else "--:--" - table.add_row("", "[yellow]Batch progress[/yellow]", batch_progress_cell, batch_elapsed, batch_remaining, f"{b['done']}/{b['total']} done") - return table - -# ---------- Remux function (updates rows in place and refreshes live) ---------- - -def remux_file_and_update(mkvmerge_path: str, src_path: Path, out_path: Path, row_idx: int, - rows, live: Live, batch_start: float, completed_times: list, log_commands: list, - skip_if_exists: bool, dry_run: bool): - """ - Perform remux and update rows[row_idx] and call live.update(build_table(...)) frequently. - Returns True/False (success), elapsed_seconds, status_string (for logging). - """ - - # prepare row - row = rows[row_idx] - row["status_text"] = "Pending" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = None - row["finished"] = False - row["success"] = False - row["start_time"] = time.time() - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # skip if exists - if skip_if_exists and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "Skipped (exists)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "Skipped (exists)" - - # identify tracks - ident = identify_tracks(mkvmerge_path, src_path) - if not ident["ok"]: - row["finished"] = True - row["success"] = False - row["status_text"] = f"FAILED identify: {ident.get('err')}" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"identify failed: {ident.get('err')}" - - # pick audio & subtitle ids - audio_ids = [] - sub_ids = [] - for t in ident["data"].get("tracks", []): - ttype = (t.get("type") or "").lower() - props = t.get("properties") or {} - lang = (props.get("language") or "").lower() - tid = t.get("id") - if ttype == "audio" and isinstance(tid, int) and lang.startswith("en"): - audio_ids.append(tid) - if ttype in ("subtitles", "subtitle") and isinstance(tid, int) and lang.startswith("en"): - sub_ids.append(tid) - - if not audio_ids: - row["finished"] = True - row["success"] = False - row["status_text"] = "FAILED (no English audio)" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, "no English audio" - - # build mkvmerge command - cmd = [mkvmerge_path, "-o", str(out_path), "--audio-tracks", ",".join(map(str, audio_ids))] - if sub_ids: - cmd += ["--subtitle-tracks", ",".join(map(str, sub_ids))] - else: - cmd += ["--no-subtitles"] - cmd += ["--ui-language", "en", "--gui-mode", str(src_path)] - log_commands.append(" ".join(cmd)) - - if dry_run: - row["pct"] = 100 - row["finished"] = True - row["success"] = True - row["status_text"] = "Dry-run (skipped actual run)" - completed_times.append(0.0) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, 0.0, "dry-run" - - # spawn mkvmerge - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, bufsize=1, encoding="utf-8", errors="replace") - except Exception as e: - row["finished"] = True - row["success"] = False - row["status_text"] = f"FAILED launch: {e}" - row["pct"] = 0 - row["elapsed"] = 0.0 - row["remaining"] = 0.0 - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, 0.0, f"launch error: {e}" - - q = queue.Queue() - _start_stdout_reader(proc.stdout, q) - re_progress = re.compile(r"(?:#GUI#progress|Progress:)\s*([0-9]{1,3})") - src_size = src_path.stat().st_size if src_path.exists() else None - had_progress = False - full_lines = [] - start = time.time() - - # live update loop - while proc.poll() is None or not q.empty(): - try: - line = q.get(timeout=0.25) - except queue.Empty: - line = None - - if line is not None: - full_lines.append(line.rstrip("\n")) - m = re_progress.search(line) - if m: - pct = int(m.group(1)) - if pct < 0: - pct = 0 - if pct > 100: - pct = 100 - had_progress = True - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - # update batch row and table - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - else: - # fallback filesize based progress (if mkvmerge doesn't emit progress) - if (not had_progress) and out_path.exists() and src_size: - try: - cur = out_path.stat().st_size - pct = int(min(99, (cur / src_size) * 100)) - except Exception: - pct = 0 - if pct > row["pct"]: - row["pct"] = pct - now = time.time() - row["elapsed"] = now - start - if pct > 0: - row["remaining"] = (row["elapsed"] * (100 - pct) / pct) - else: - row["remaining"] = None - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - # else just loop - - rc = proc.wait() - elapsed = time.time() - start - tail = "\n".join(full_lines[-12:]) if full_lines else "" - if rc == 0 and out_path.exists() and out_path.stat().st_size > 0: - row["pct"] = 100 - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = True - row["status_text"] = "OK" - completed_times.append(elapsed) - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return True, elapsed, "OK" - else: - row["pct"] = min(100, row.get("pct", 0)) - row["elapsed"] = elapsed - row["remaining"] = 0.0 - row["finished"] = True - row["success"] = False - reason = f"rc={rc}" - if tail: - reason += f" tail='{tail.replace(chr(10),' | ')[:200]}'" - row["status_text"] = f"FAILED ({reason})" - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - return False, elapsed, reason - -# ---------- Batch helper ---------- - -def compute_batch(rows, batch_start, completed_times): - total = len(rows) - done = sum(1 for r in rows if r.get("finished") and r.get("success")) - # batch pct - average of per-file pct - avg_pct = int(sum(r.get("pct", 0) for r in rows) / total) if total > 0 else 0 - elapsed = time.time() - batch_start - # estimate remaining: use avg of completed times if available; account for current partial progress - remaining = None - if len(completed_times) > 0: - avg_time = sum(completed_times) / len(completed_times) - remaining_files = total - sum(1 for r in rows if r.get("finished")) - # estimate for the currently running file's remaining (if any) - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - if current_remaining is None: - current_remaining = 0.0 - remaining = current_remaining + max(0, remaining_files - 1) * avg_time - else: - # no completed times yet -> try to estimate from current file only - current_remaining = None - for r in rows: - if (not r.get("finished")) and r.get("pct", 0) > 0: - pct = r["pct"] - if pct > 0: - current_elapsed = r.get("elapsed", 0.0) - current_remaining = current_elapsed * (100 - pct) / pct - break - remaining = current_remaining - return {"pct": avg_pct, "elapsed": elapsed, "remaining": remaining, "done": sum(1 for r in rows if r.get("finished")), "total": total, "finished": all(r.get("finished") for r in rows)} - -# ---------- Main ---------- - -def main(): - console.print("[bold cyan]=== MKVToolNix batch remux (English only) ===[/]") - - inp = input("Enter input directory path: ").strip().strip('"') - if not inp: - console.print("[red]Input directory required.[/]") - return - input_dir = Path(inp) - if not input_dir.exists() or not input_dir.is_dir(): - console.print(f"[red]Directory not found: {input_dir}[/]") - return - - outp = input("Enter output directory path (blank = remuxed): ").strip().strip('"') - output_dir = Path(outp) if outp else input_dir / "remuxed" - output_dir.mkdir(parents=True, exist_ok=True) - skip_if_exists = (input("Skip files if output exists? (Y/n) [Y]: ").strip().lower() != "n") - dry_run = (input("Dry-run only? (y/N) [N]: ").strip().lower() == "y") - - mkvmerge_path = find_mkvmerge() - if not mkvmerge_path: - console.print("[red]❌ mkvmerge not found on PATH. Install MKVToolNix or add mkvmerge to PATH.[/]") - return - - # collect mkv files - mkv_files = sorted(input_dir.glob("*.mkv")) - total = len(mkv_files) - if total == 0: - console.print("[yellow]No .mkv files found in the input directory.[/]") - return - - console.print(f"Found {total} MKV file(s) in: {input_dir}\n") - - # prepare rows - rows = [] - for i, f in enumerate(mkv_files, start=1): - rows.append({ - "no": i, - "fullname": f.name, - "display_name": shorten(f.name, 40), - "pct": 0, - "elapsed": 0.0, - "remaining": None, - "status_text": "Pending", - "finished": False, - "success": False, - "start_time": None - }) - - log_path = output_dir / "remux_log.txt" - with open(log_path, "a", encoding="utf-8") as lf: - lf.write(f"==== remux run: {time.strftime('%Y-%m-%d %H:%M:%S')} ====\n") - lf.write(f"Input dir: {input_dir}\nOutput dir: {output_dir}\nFiles: {total}\n\n") - - # store executed commands for debug (optional) - log_commands = [] - - # run live table - batch_start = time.time() - completed_times = [] - - with Live(build_table(rows, compute_batch(rows, batch_start, completed_times)), refresh_per_second=8, console=console) as live: - for idx, src in enumerate(mkv_files): - out_file = output_dir / src.name - ok, elapsed, reason = remux_file_and_update( - mkvmerge_path, src, out_file, idx, rows, live, batch_start, completed_times, log_commands, - skip_if_exists, dry_run - ) - # status already updated inside function; ensure last render - live.update(build_table(rows, compute_batch(rows, batch_start, completed_times))) - - # write final plain-text summary table to log - final_lines = [] - final_lines.append("No\tFilename\tProgress\tElapsed\tRemaining\tStatus") - ok_count = 0 - fail_count = 0 - for r in rows: - status_text = r.get("status_text", "") - if r.get("success"): - ok_count += 1 - elif r.get("finished") and not r.get("success"): - fail_count += 1 - final_lines.append(f"{r['no']}\t{r['fullname']}\t{int(r.get('pct',0))}%\t{fmt_time(r.get('elapsed'))}\t{fmt_time(r.get('remaining')) if r.get('remaining') is not None else '--:--'}\t{status_text}") - - total_elapsed = time.time() - batch_start - - with open(log_path, "a", encoding="utf-8") as lf: - lf.write("\n".join(final_lines) + "\n\n") - lf.write(f"=== Summary ===\nProcessed: {total}, OK: {ok_count}, Failed: {fail_count}\nTotal time: {fmt_time(total_elapsed)}\n") - if log_commands: - lf.write("\nCommands executed (sample):\n") - for c in log_commands[:10]: - lf.write(c + "\n") - lf.write("\n") - - console.print(f"\n[green]Completed: {ok_count} OK, {fail_count} failed. Log saved to:[/] {log_path}") - -if __name__ == "__main__": - main() diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..31f1b28 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,25 @@ +# Frequently Asked Questions (FAQ) + +## Installation +1. **How do I install the project?** + Follow the installation instructions in the README file. Ensure you have all dependencies installed. + +## Usage +1. **How do I use the project?** + Check the usage section in the README file for examples and command-line options. + +## Troubleshooting +1. **What should I do if I encounter errors?** + Refer to the troubleshooting section in the documentation. Common errors and their solutions are listed there. + +## Performance +1. **How can I improve performance?** + Ensure that you are using the latest version, and refer to the performance tuning section for best practices. + +## Supported Formats +1. **What formats does the project support?** + The project supports various formats including JSON, XML, and CSV. Check the documentation for the complete list. + +## Contribution Guidelines +1. **How can I contribute to the project?** + Contributions are welcome! Please read the contribution guidelines in the repository for more details. \ No newline at end of file diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..8e2320e --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,80 @@ +# Installation Instructions + +## System Requirements +- **Windows:** + - OS: Windows 10 or later + - RAM: Minimum 4 GB (8 GB recommended) + - Disk Space: 500 MB free space + +- **macOS:** + - OS: macOS Mojave (10.14) or later + - RAM: Minimum 4 GB (8 GB recommended) + - Disk Space: 500 MB free space + +- **Linux:** + - OS: Ubuntu 18.04 or later, or Fedora 30 or later + - RAM: Minimum 4 GB (8 GB recommended) + - Disk Space: 500 MB free space + +## Installation Steps + +### Windows +1. **Install Python:** + - Download Python from [python.org](https://www.python.org/downloads/) + - Run the installer and check "Add Python to PATH". + - Verify installation by running `python --version` in Command Prompt. + +2. **Install MKVToolNix:** + - Download MKVToolNix from [MKVToolNix official site](https://mkvtoolnix.download/downloads.html). + - Run the installer and follow the instructions. + +### macOS +1. **Install Python:** + - Open Terminal and run: + ```bash + brew install python + ``` + - Verify installation by running `python3 --version`. + +2. **Install MKVToolNix:** + - Download MKVToolNix from [MKVToolNix official site](https://mkvtoolnix.download/downloads.html). + - Open the downloaded `.dmg` file and drag MKVToolNix to Applications. + +### Linux +1. **Install Python:** + - For Ubuntu: + ```bash + sudo apt update + sudo apt install python3 + ``` + - For Fedora: + ```bash + sudo dnf install python3 + ``` + - Verify installation by running `python3 --version`. + +2. **Install MKVToolNix:** + - For Ubuntu: + ```bash + sudo add-apt-repository ppa:abogdan + sudo apt update + sudo apt install mkvtoolnix mkvtoolnix-gui + ``` + - For Fedora: + ```bash + sudo dnf install mkvtoolnix mkvtoolnix-gui + ``` + +## Verification Procedures +- For Python, run `python --version` or `python3 --version` in the terminal/command prompt. +- For MKVToolNix, open the application and check the version in the Help menu. + +## Troubleshooting +- **Python not recognized:** + - Ensure Python is added to the PATH during installation. + +- **MKVToolNix fails to open:** + - Check if the installation was completed successfully and if your system meets the requirements. + +- **Common installation errors:** + - Refer to the official documentation for Python and MKVToolNix for troubleshooting guides. \ No newline at end of file diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..ea577e1 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,31 @@ +# Usage Guide + +## Quick Start +- Brief introduction on how to get started with the project. + +## GUI Walkthrough +- Step-by-step guide on using the graphical user interface. +- Diagrams to illustrate key functionalities. + +## CLI Prompts Guide +- Detailed explanation of command-line interface prompts and commands. + +## Language Codes Reference Table +| Language | Code | +|----------|------| +| English | en | +| Spanish | es | +| French | fr | +| ... | ... | + +## Advanced Usage Scenarios +- Examples of advanced usage to help users maximize the functionality. + +## Best Practices +- Recommendations on how to effectively use the project. + +## Common Use Cases +- List of typical scenarios where the project can be applied. + +## Performance Tips +- Tips for optimizing performance when using the project. \ No newline at end of file