From 9b0c557d9cb23449dcb4a7188e8ffbc7652dcd39 Mon Sep 17 00:00:00 2001 From: Ramforth Date: Sun, 2 Nov 2025 12:37:09 +0100 Subject: [PATCH] feat: Implement GUI and standalone executable --- DEVELOPMENT_PLAN.md | 124 ++++-------------- README.md | 8 +- USAGE.md | 102 ++++++--------- src/__init__.py => __init__.py | 0 src/config.py => config.py | 26 +++- converter.py | 108 ++++++++++++++++ downloader.py | 99 ++++++++------- gui.py | 207 ++++++++++++++++++++++++++++++ main.py | 224 ++++++++++++++++++--------------- requirements.txt | 2 +- src/converter.py | 99 --------------- src/main.py | 75 ----------- src/utils.py => utils.py | 17 ++- video-converter.spec | 6 +- 14 files changed, 605 insertions(+), 492 deletions(-) rename src/__init__.py => __init__.py (100%) rename src/config.py => config.py (56%) create mode 100644 converter.py create mode 100644 gui.py delete mode 100644 src/converter.py delete mode 100644 src/main.py rename src/utils.py => utils.py (83%) diff --git a/DEVELOPMENT_PLAN.md b/DEVELOPMENT_PLAN.md index 57d724f..f6d0664 100644 --- a/DEVELOPMENT_PLAN.md +++ b/DEVELOPMENT_PLAN.md @@ -1,108 +1,36 @@ -# Video Converter Script Development Plan +## Development Plan: Video Converter -## 1. Project Goal -Create a standalone executable application (using Python) to convert video files into formats highly compatible with Davinci Resolve, ensuring optimal aspect ratio, video quality, and audio fidelity. The application should be user-friendly, accepting input file paths via CLI arguments, prompting for output locations (if not specified), and leveraging `ffmpeg` for robust conversion. The goal is a single executable that requires no external dependencies (like Python or `ffmpeg` installations) from the end-user. +This document outlines the development plan for the Video Converter project, aiming to create a standalone Python tool for converting video files into DaVinci Resolve compatible formats. -## 2. Key Tools and Technologies -* **Python:** For scripting the user interface, logic, and orchestrating `ffmpeg` commands. -* **`ffmpeg` & `ffprobe`:** The primary tools for video and audio conversion and analysis. These will be bundled with the final application, and are called directly via Python's `subprocess` module. -* **`yt-dlp`:** A Python library used for downloading videos from various online platforms. -* **PyInstaller (or similar):** For packaging the Python script and bundled `ffmpeg`/`ffprobe` into a standalone executable. +### Overall Goal +Develop a user-friendly, standalone Python tool to convert video files into formats compatible with Davinci Resolve (Free edition on Linux) to improve editing workflows for content creators. -**Inspiration Repositories:** We will review `xavier150/convert-video-for-Resolve` and `tkmxqrdxddd/davinci-video-converter` for insights into Davinci Resolve specific conversion strategies and `ffmpeg` command construction. +### Completed Features -## 3. Core Functionality and Requirements +* **Core Conversion Logic:** Implemented video conversion using `ffmpeg` with `dnxhd` codec and `pcm_s16le` audio, outputting to `.mov` container. +* **Quality Profile Picker:** Added `--quality` argument (low, medium, high, archive) to control `dnxhd` profiles. +* **Filename Fail-safe:** Implemented logic to append `_1`, `_2`, etc., to output filenames if a file with the same name already exists. +* **`yt-dlp` Integration:** Added `--url` argument to download videos from YouTube (and other supported sites) using `yt-dlp` before conversion. +* **PyInstaller Bundling:** Successfully bundled the application into a standalone executable for Linux. +* **Robust Download Path Handling:** Ensured `yt-dlp` downloads directly to the user-specified output directory (or current working directory if not specified), resolving `os.path.exists()` issues within the PyInstaller environment. +* **Basic Error Handling & Logging:** Suppressed verbose `yt-dlp` output and redirected `ffmpeg` output to `ffmpeg_output.log` for detailed error inspection. -### 3.1. User Interaction -* **Input Source Selection:** The script accepts either a local file path or a URL via command-line arguments (`--url` for URL, positional argument for file path). If neither is provided, the user is interactively prompted to enter a file path or URL. -* **Output Directory Selection:** The script asks the user for a desired output directory via the `-o` argument. If no directory is provided, it defaults to the input file's (or downloaded file's) directory. -* **Output File Naming:** Converted files are named clearly. If the output directory is the same as the input file's directory, the output filename is prefixed with `recoded_` (e.g., `recoded_original_file.mov`). Otherwise, a clear naming convention is applied (e.g., append `_DR_compatible`). A fail-safe is implemented to generate unique filenames if the target file already exists. -* **Quality Profile Selection:** Users can select the output video quality profile via the `-q` argument (`low`, `medium`, `high`, `archive`), which maps to specific DNxHD/HR profiles based on resolution. +### Current Priorities (Next Steps) -### 3.2. Shell Environment (Re-evaluation) -* The initial request mentioned confirming the shell type. However, `ffmpeg` commands executed via Python's `subprocess` module are generally shell-agnostic. The Python script itself will handle the execution. Therefore, explicit shell detection is likely unnecessary unless specific shell-dependent environment variables or configurations for `ffmpeg` or `yt-dlp` (if integrated later) become an issue. This point will be kept in mind for troubleshooting. +1. **Implement GUI:** Develop a graphical user interface for easier interaction, including file selection, quality profile choice, and URL input. (High Priority) +2. **Implement Browser Cookies for YouTube Authorization:** Revisit and implement `--cookies-from-browser` functionality for `yt-dlp` to handle age-restricted or private YouTube videos. (Deferred, now higher priority) +3. **Refine Error Handling/Logging:** Implement more user-friendly error messages, potentially displaying the last few lines of the log file directly in the console upon failure, and providing clear instructions to check the full log. -### 3.3. Davinci Resolve Compatibility - Codec and Format Selection +### Future Considerations -Based on the "DaVinci Resolve 18 Supported Codec List.pdf" and web search results, the following are recommended for optimal compatibility and editing performance in Davinci Resolve, especially for intermediate files: +* **Batch Processing:** Allow conversion of multiple files or URLs in a single run. +* **Configuration File:** Implement a configuration file (e.g., YAML, JSON) for persistent settings. +* **Progress Bar:** Integrate a progress bar for both download and conversion processes. +* **Cross-Platform Compatibility:** Explore bundling for Windows and macOS. (Lowest priority) +* **Advanced `ffmpeg` Options:** Expose more `ffmpeg` options (e.g., bitrate, resolution scaling) for advanced users. +* **Metadata Preservation:** Option to preserve or transfer metadata from the original file. -#### Video Codecs (for Intermediate Editing) -* **DNxHR / DNxHD:** - * **Container:** QuickTime (`.mov`) - * **Description:** Excellent intraframe codecs for Windows and Linux. DNxHR is for resolutions above 1080p, while DNxHD is for up to 1080p. They offer good performance and preserve quality. - * **Profiles:** HQ (High Quality) or HQX (Higher Quality, larger file size) are preferred. - * **FFmpeg Example:** `-c:v dnxhr -profile:v HQ` (or `HQX`) -* **Apple ProRes:** - * **Container:** QuickTime (`.mov`) - * **Description:** High-quality, widely used, especially on macOS. FFmpeg can encode/decode on other OS. Davinci Resolve supports ProRes with alpha channels. - * **FFmpeg Example:** `-c:v prores_ks -profile:v 3` (for ProRes HQ, profile numbers vary) -* **GoPro CineForm:** - * **Container:** QuickTime (`.mov`) - * **Description:** Another high-quality intermediate codec supported by Davinci Resolve. +### Known Issues / Limitations -#### Audio Codecs -* **PCM (Pulse Code Modulation):** - * **Description:** Crucial for Linux users, as Davinci Resolve on Linux often has compatibility issues with AAC audio. PCM is uncompressed and widely compatible. - * **FFmpeg Example:** `-c:a pcm_s16le` (16-bit signed little-endian PCM) -* **FLAC:** - * **Description:** A lossless audio codec, suitable as an alternative to PCM or for extracting audio separately if needed. - * **FFmpeg Example:** `-c:a flac` - -#### Container Format -* **QuickTime (`.mov`):** Generally recommended for intermediate files due to better stability and metadata handling with Davinci Resolve. - -#### Quality and Settings -* **Aspect Ratio:** The script should preserve the original aspect ratio of the input video. `ffmpeg` handles this by default unless specific scaling filters are applied. -* **Resolution:** Maintain original resolution or convert to a Davinci Resolve-friendly resolution if necessary (e.g., for DNxHD/HR profiles). -* **Bit Depth:** Aim for 10-bit color depth (e.g., `yuv422p10le` pixel format) if the source video supports it and Davinci Resolve Studio is used, to prevent banding and ensure higher quality. -* **Bitrate:** For intermediate files, high bitrates are expected and desired to preserve quality. - -### 3.4. Conversion Logic (using `ffmpeg`) -1. **Analyze Input:** Use `ffprobe` to get detailed information about the input video (video codec, audio codec, resolution, aspect ratio, frame rate, bit depth). -2. **Determine Output Parameters:** Based on the input analysis and user preferences (if any are added later), select the most appropriate Davinci Resolve compatible video and audio codecs, profiles, and pixel formats. -3. **Construct `ffmpeg` Command:** Build the `ffmpeg` command dynamically. - * **Example for DNxHR/DNxHD with PCM audio:** - ```bash - ffmpeg -i "input.mp4" -c:v dnxhr -profile:v HQ -c:a pcm_s16le "output_DR_compatible.mov" - ``` - * **Considerations:** - * Handling different input audio codecs (e.g., converting AAC to PCM). - * Mapping all video and audio streams (`-map 0`). - * Potentially adding `-vf scale=w:h:force_original_aspect_ratio=decrease,pad=w:h:(ow-iw)/2:(oh-ih)/2` for specific aspect ratio handling if cropping/padding is desired, but generally, preserving original is default. - -## 4. Development Steps (Completed) - -1. **Initial Script Setup:** Created `main.py` with `argparse` for CLI arguments, including input file/URL, output directory, and quality profile selection. -2. **Directory Creation:** Ensured output directory exists. -3. **`ffprobe` Integration:** Implemented functions in `utils.py` to call `ffprobe` via `subprocess` and parse its JSON output to get video/audio stream details. -4. **`ffmpeg` Command Construction:** Developed logic in `converter.py` to build the `ffmpeg` command as a list of strings based on `ffprobe` output, desired Davinci Resolve compatibility, and selected quality profile. -5. **`ffmpeg` Execution:** Used `subprocess.run()` in `converter.py` to execute the `ffmpeg` command, capturing stdout/stderr for logging. -6. **Error Handling:** Added robust `try-except` blocks for file operations, `subprocess` calls, and `ffprobe` parsing. Implemented a fail-safe for existing output filenames by generating unique names. -7. **Basic Testing:** Conducted manual tests with sample video files to ensure conversion to DNxHD/PCM MOV works, including audio, and refined quality profiles. -8. **Packaging and Bundling:** Used PyInstaller to create a standalone executable, bundling `ffmpeg` and `ffprobe` binaries. This involved switching from `ffmpeg-python` to direct `subprocess` calls to resolve bundling issues. The executable is now functional. -## 5. Current Status - -The core functionality of the Video Converter is complete. The script successfully converts video files to DaVinci Resolve compatible formats (DNxHD/HR video, PCM audio in .mov container) and is packaged as a standalone executable for Linux. Error handling has been strengthened, including a fail-safe for existing output filenames, and the default quality profiles have been refined based on user feedback. - -## 6. Future Development Priorities - -Based on user feedback and project goals, the next development priorities are: - -1. **`yt-dlp` Integration:** (Completed) - * **Goal:** Allow users to directly download videos from supported online platforms (e.g., YouTube) and then convert them to DaVinci Resolve compatible formats in a single workflow. - * **Details:** Integrated `yt-dlp` to handle video downloading. The script now accepts a URL via the `--url` argument or interactively, downloads the video to a temporary location (or specified output directory), and then proceeds with the existing conversion process. - -2. **Quality Profile Picker (Command-Line Option):** (Completed) - * **Goal:** Provide users with command-line options to select different quality/file size profiles for the output video (e.g., `dnxhr_lb`, `dnxhr_sq`, `dnxhr_hq`, `dnxhr_hqx`). - * **Details:** Added a new `argparse` argument (`--quality`) that maps to specific `dnxhd` profiles (`low`, `medium`, `high`, `archive`) based on resolution, giving users control over the size/quality trade-off. - -3. **Graphical User Interface (GUI):** - * **Goal:** Develop a user-friendly graphical interface for the application. - * **Details:** This would involve choosing a Python GUI framework (e.g., `tkinter`, `PyQt`, `Kivy`) and designing an interface that allows users to select input/output files, choose quality profiles, view conversion progress, and manage other settings visually. This would significantly enhance the user experience for non-CLI users. - -## 7. Other Future Considerations - -* **Batch Processing:** Allow conversion of multiple files at once. -* **Refine Error Handling/Logging:** Add more detailed logging to a file for easier debugging. -* **Configuration File:** Enable users to save preferred codec/profile settings. -* **Progress Bar:** Implement a progress indicator for long conversions. +* DaVinci Resolve Free on Linux has limitations with certain audio codecs (e.g., AAC) and 4K output/encoding (requires Studio version). +* `ffmpeg-full` AUR package is required for `dnxhd` support on Arch-based systems. \ No newline at end of file diff --git a/README.md b/README.md index 0c42f7b..c255a62 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Video Converter for DaVinci Resolve -This project aims to develop a standalone Python application to convert video files into formats highly compatible with DaVinci Resolve (Free edition on Linux). The goal is to provide a simple command-line tool that ensures optimal video quality, aspect ratio, and audio fidelity for editing workflows. +This project is a standalone Python application with a graphical user interface (GUI) to convert video files into formats highly compatible with DaVinci Resolve (Free edition on Linux). The goal is to provide a simple and user-friendly tool that ensures optimal video quality, aspect ratio, and audio fidelity for editing workflows. + +The application allows you to: +* Convert local video files or videos from URLs. +* Select different quality profiles. +* Use browser cookies to download age-restricted or private videos from sites like YouTube. +* See the conversion progress in real-time. For a detailed breakdown of the project's goals, technical specifications, and development roadmap, please refer to the [DEVELOPMENT_PLAN.md](DEVELOPMENT_PLAN.md) file. diff --git a/USAGE.md b/USAGE.md index 1066170..ec64b00 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,96 +1,70 @@ -# How to Use the Video Converter (Current State) +# How to Use the Video Converter -This document outlines the steps required to set up and run the Video Converter script in its current development state. +This document outlines the steps required to set up and run the Video Converter application. ## 1. Prerequisites -Before running the script, ensure you have the following installed on your **Arch Linux / CachyOS** system: +Before running the application, ensure you have the following installed on your **Arch Linux / CachyOS** system: -* **Python 3:** The script is written in Python 3. * **`ffmpeg-full`:** A version of `ffmpeg` compiled with DNxHD/HR support. * Install from AUR: `yay -S ffmpeg-full` (or your preferred AUR helper). * **Note:** If you use OBS Studio, refer to the [README.md](README.md) for potential dependency workarounds. +* **`yt-dlp`**: A command-line program to download videos from YouTube and other sites. + * Install from the official repositories: `sudo pacman -S yt-dlp` ## 2. Setup -1. **Clone the Repository:** - ```bash - git clone https://gitea.ramforth.net/ramforth/video-converter.git - cd video-converter - ``` +1. **Download the application:** + You can either clone the repository and build the application yourself, or download the latest release from the project's Gitea page. -2. **Create and Activate a Python Virtual Environment:** - It's highly recommended to use a virtual environment to manage Python dependencies. +2. **Building from source (optional):** + If you have cloned the repository, you can build the executable by running the following commands: ```bash - python3 -m venv venv + # Activate the virtual environment source venv/bin/activate + + # Build the executable + pyinstaller --noconfirm video-converter.spec ``` -3. **Install Python Dependencies:** - ```bash - pip install -r requirements.txt - ``` +## 3. Running the Application -## 3. Running the Converter - -Once the setup is complete, you can run the converter script or the packaged executable. - -### Running the Python Script - -```bash -python main.py [path_to_your_input_video_file] [-u ] [-q ] [-o ] -``` - -* Replace `[path_to_your_input_video_file]` with the absolute or relative path to the video file you want to convert. This argument is now optional. -* Use `-u ` to provide a URL for a video to download and convert. If both a file path and a URL are provided, the URL will be prioritized. -* Use `-q ` to select the output video quality. Available choices are `low`, `medium` (default), `high`, and `archive`. This controls the DNxHD/HR profile used for conversion, impacting file size and visual fidelity. -* Replace `` with the desired directory for the converted file. If omitted, the converted file will be saved in the same directory as the input file (or the downloaded file). - -**Example (Interactive Input):** - -```bash -python main.py -# Script will then prompt: Please enter the path to the input video file or a URL: -``` - -**Example (Downloading and Converting from URL):** - -```bash -python main.py -u "https://www.youtube.com/watch?v=dQw4w9WgXcQ" -q high -o /home/user/ConvertedVideos -``` - -### Running the Packaged Executable - -After building the executable (see `DEVELOPMENT_PLAN.md` for details on packaging), you can find it in the `dist/` directory. +Once you have the executable, you can run it from your terminal. 1. **Navigate to the `dist` directory:** ```bash cd /path/to/your/project/video-converter/dist ``` -2. **Run the executable:** + +2. **Make the executable runnable (if necessary):** ```bash - ./video-converter [path_to_your_input_video_file] [-u ] [-q ] [-o ] + chmod +x video-converter ``` - The usage is identical to the Python script, but you execute the binary directly. - - **Example:** +3. **Run the executable:** ```bash - ./video-converter /home/user/Videos/my_original_video.mp4 -q low -o /home/user/ConvertedVideos - ./video-converter -u "https://www.youtube.com/watch?v=dQw4w9WgXcQ" -q archive + ./video-converter ``` -## 4. What it Does +This will launch the graphical user interface (GUI). -The script converts your input video into a DaVinci Resolve-compatible format using `ffmpeg`: +## 4. Using the GUI -* **Video Codec:** `dnxhd` (DNxHD/HR) - * **1080p (<=1080 height):** `dnxhr_sq` (Standard Quality) - * **1440p (>1080 height):** `dnxhr_hq` (High Quality) -* **Audio Codec:** `pcm_s16le` (Linear PCM) -* **Container:** `.mov` (QuickTime) +The GUI provides the following options: -This ensures optimal compatibility and performance for editing in DaVinci Resolve (Free Linux version). +* **Select video file or enter URL:** + * **Browse:** Click this button to open a file dialog and select a local video file to convert. + * **URL:** Enter the URL of a video to download and convert. +* **Quality:** Select the output video quality profile. The available options are `low`, `medium`, `high`, and `archive`. +* **Cookies from Browser:** If you are downloading a video that requires you to be logged in (e.g., an age-restricted YouTube video), you can select the browser from which to extract the cookies. +* **Output Directory:** Click the "Select Directory" button to choose the directory where the converted file will be saved. +* **Convert:** Click this button to start the conversion process. +* **Cancel:** This button will be enabled during the conversion process and can be used to request cancellation. +* **Status Bar:** The status bar at the bottom of the window will show the current status of the application, including download and conversion progress. +* **Progress Bar:** The progress bar will show the real-time progress of the conversion. ---- -*Document generated by Gemini CLI Agent.* \ No newline at end of file +## 5. Logging and Error Reporting + +During the conversion, `ffmpeg`'s output is logged to a file named `ffmpeg_output.log` in the same directory as the executable. + +If an error occurs during the conversion, a message box will appear with the error message and the last 10 lines of the log file. This can help you diagnose the problem. \ No newline at end of file diff --git a/src/__init__.py b/__init__.py similarity index 100% rename from src/__init__.py rename to __init__.py diff --git a/src/config.py b/config.py similarity index 56% rename from src/config.py rename to config.py index 096daeb..766a64d 100644 --- a/src/config.py +++ b/config.py @@ -6,8 +6,29 @@ import os # Target 1080p60 and 1440p60 DEFAULT_VIDEO_CODEC = "dnxhd" -DEFAULT_VIDEO_PROFILE_1080P = "HQ" # DNxHD HQ for 1080p -DEFAULT_VIDEO_PROFILE_1440P = "HQ" # DNxHR HQ for 1440p + +# DNxHD profile mapping for different quality levels and resolutions +DNXHD_PROFILES = { + 'low': { + '1080p': "dnxhr_lb", # Low Bandwidth + '1440p': "dnxhr_sq", # Standard Quality (for higher res, LB might be too low) + }, + 'medium': { + '1080p': "dnxhr_sq", # Standard Quality + '1440p': "dnxhr_hq", # High Quality + }, + 'high': { + '1080p': "dnxhr_hq", # High Quality + '1440p': "dnxhr_hqx", # High Quality X + }, + 'archive': { + '1080p': "dnxhr_hqx", # High Quality X + '1440p': "dnxhr_444", # 4:4:4 (highest quality) + }, +} + +DEFAULT_QUALITY_LEVEL = 'medium' # Used if no quality is specified + DEFAULT_VIDEO_PIX_FMT = "yuv422p" # Common for DNxHD/HR DEFAULT_CONTAINER = ".mov" @@ -28,6 +49,7 @@ FFPROBE_GLOBAL_OPTIONS = [ # For development, assume they are in PATH FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "ffmpeg") FFPROBE_PATH = os.environ.get("FFPROBE_PATH", "ffprobe") +FFMPEG_LOG_FILE_PATH = "ffmpeg_output.log" # Output file naming conventions OUTPUT_SUFFIX = "_DR_compatible" diff --git a/converter.py b/converter.py new file mode 100644 index 0000000..72a2974 --- /dev/null +++ b/converter.py @@ -0,0 +1,108 @@ +import os +import subprocess +import sys + +import utils +import config + +def convert_video( + input_file_path, + output_dir, + quality_level=config.DEFAULT_QUALITY_LEVEL, + video_codec=config.DEFAULT_VIDEO_CODEC, + audio_codec=config.DEFAULT_AUDIO_CODEC, + output_suffix=config.OUTPUT_SUFFIX, + output_prefix=config.OUTPUT_PREFIX, + output_ext=config.DEFAULT_CONTAINER +): + """Converts a video file to a DaVinci Resolve compatible format.""" + print(f"Analyzing input file: {input_file_path}") + video_info = utils.get_video_info(input_file_path) + audio_info = utils.get_audio_info(input_file_path) + + if not video_info or not video_info.get("streams"): + print("Error: Could not retrieve video stream information. Aborting conversion.", file=sys.stderr) + return False + + # Extract relevant video stream info + try: + v_stream = video_info["streams"][0] + width = v_stream["width"] + height = v_stream["height"] + except (KeyError, IndexError) as e: + print(f"Error parsing video stream info: {e}", file=sys.stderr) + return False + + # Determine video profile based on resolution and quality level + target_resolution_key = '1080p' + if height > 1080: + target_resolution_key = '1440p' + + video_profile = config.DNXHD_PROFILES[quality_level][target_resolution_key] + + output_file_path = utils.generate_output_path( + input_file_path, output_dir, output_suffix, output_prefix, output_ext + ) + + # Fail-safe: Generate unique filename if target already exists + counter = 0 + original_output_file_path = output_file_path + while os.path.exists(output_file_path): + counter += 1 + name, ext = os.path.splitext(original_output_file_path) + output_file_path = f"{name}_{counter}{ext}" + print(f"Warning: Output file '{original_output_file_path}' already exists. Trying '{output_file_path}'.", file=sys.stderr) + + print(f"Converting '{input_file_path}' to '{output_file_path}'") + + # Construct FFmpeg command + ffmpeg_command = [ + config.FFMPEG_PATH, + *config.FFMPEG_GLOBAL_OPTIONS, + "-progress", "ffmpeg_progress.log", + "-i", input_file_path, + "-c:v", video_codec, + "-profile:v", video_profile, + "-pix_fmt", config.DEFAULT_VIDEO_PIX_FMT, + ] + + # Add audio options if audio_info is available + if audio_info and audio_info.get("streams"): + ffmpeg_command.extend([ + "-c:a", audio_codec, + ]) + else: + print("Warning: No audio stream found or parsed. Output will be video-only.", file=sys.stderr) + ffmpeg_command.append("-an") # Disable audio if no audio stream + + ffmpeg_command.append(output_file_path) + + try: + with open(config.FFMPEG_LOG_FILE_PATH, "w") as log_file: + process = subprocess.run( + ffmpeg_command, + stdout=log_file, + stderr=log_file, + text=True, + check=True + ) + return True + except subprocess.CalledProcessError as e: + print(f"Error during FFmpeg conversion: {e}", file=sys.stderr) + print(f"Check {config.FFMPEG_LOG_FILE_PATH} for details.", file=sys.stderr) + # Optionally print last few lines of log for quick debugging + try: + with open(config.FFMPEG_LOG_FILE_PATH, "r") as log_file: + lines = log_file.readlines() + print("Last 10 lines of FFmpeg log:", file=sys.stderr) + for line in lines[-10:]: + print(line.strip(), file=sys.stderr) + except Exception as log_err: + print(f"Could not read FFmpeg log file: {log_err}", file=sys.stderr) + return False + except FileNotFoundError: + print("Error: FFmpeg not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr) + return False + except Exception as e: + print(f"An unexpected error occurred: {e}", file=sys.stderr) + return False \ No newline at end of file diff --git a/downloader.py b/downloader.py index c1528da..02fe9fc 100644 --- a/downloader.py +++ b/downloader.py @@ -1,9 +1,11 @@ -import yt_dlp import os import sys import tempfile +import subprocess +import re +import time -def download_video(url, output_dir=None): +def download_video(url, output_dir=None, cookies_from_browser=None): """Downloads a video from the given URL to a temporary location or specified output directory. Args: @@ -13,59 +15,66 @@ def download_video(url, output_dir=None): Returns: str: The absolute path to the downloaded video file, or None if download fails. """ - if output_dir and not os.path.isdir(output_dir): + if output_dir is None: + output_dir = os.getcwd() + + if not os.path.isdir(output_dir): print(f"Error: Output directory '{output_dir}' for download does not exist or is not a directory.", file=sys.stderr) return None - temp_dir = None - if not output_dir: - temp_dir = tempfile.TemporaryDirectory() # Create a temporary directory - output_dir = temp_dir.name + # Construct yt-dlp command + download_filename = "yt_dlp_downloaded_video.mp4" + yt_dlp_command = [ + "yt-dlp", + "--format", "bestvideo[ext!=webm]+bestaudio[ext!=webm]/best[ext!=webm]", # Prioritize non-webm formats + "--output", os.path.join(output_dir, download_filename), + "--no-playlist", # Only download single video, not entire playlist + "--quiet", + "--no-warnings", + url + ] - ydl_opts = { - 'format': 'bestvideo[ext!=webm]+bestaudio[ext!=webm]/best[ext!=webm]', # Prioritize non-webm formats - 'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'), - 'noplaylist': True, # Only download single video, not entire playlist - 'progress_hooks': [lambda d: sys.stdout.write(f"Downloading: {d['filename']} - {d['status']}" + '\r') if d['status'] == 'downloading' else None], - 'postprocessors': [{ - 'key': 'FFmpegVideoConvertor', - 'preferedformat': 'mp4', - }], - 'quiet': True, # Suppress yt-dlp output unless error - 'no_warnings': True, - } + if cookies_from_browser and cookies_from_browser != "none": + yt_dlp_command.extend(["--cookies-from-browser", cookies_from_browser]) downloaded_file_path = None try: - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info_dict = ydl.extract_info(url, download=True) - downloaded_file_path = ydl.prepare_filename(info_dict) - # yt-dlp might return a path with a different extension if format conversion happened - # We need to find the actual downloaded file - base, ext = os.path.splitext(downloaded_file_path) - if not os.path.exists(downloaded_file_path): - # Try common extensions if the exact path isn't found - for common_ext in ['.mp4', '.mkv', '.webm', '.flv']: - if os.path.exists(base + common_ext): - downloaded_file_path = base + common_ext - break - - if not os.path.exists(downloaded_file_path): - print(f"Error: Downloaded file not found at expected path: {downloaded_file_path}", file=sys.stderr) - return None + # print(f"Executing yt-dlp command: {' '.join(yt_dlp_command)}", file=sys.stderr) + process = subprocess.run( + yt_dlp_command, + capture_output=True, + text=True, + check=True + ) + # print(f"DEBUG: yt-dlp stdout: {process.stdout}", file=sys.stderr) + # print(f"DEBUG: yt-dlp stderr: {process.stderr}", file=sys.stderr) - except yt_dlp.utils.DownloadError as e: - print(f"Error downloading video: {e}", file=sys.stderr) + # The downloaded file path is now explicitly set + downloaded_file_path = os.path.join(output_dir, download_filename) + + # print(f"DEBUG: Parsed downloaded_file_path: {downloaded_file_path}", file=sys.stderr) + + # Add retry logic for file existence + max_retries = 5 + retry_delay = 1 # seconds + for i in range(max_retries): + if downloaded_file_path and os.path.exists(downloaded_file_path): + break + # print(f"DEBUG: Waiting for downloaded file to appear... (Attempt {i+1}/{max_retries})", file=sys.stderr) + time.sleep(retry_delay) + else: + print(f"Error: Downloaded file not found at expected path after {max_retries} retries: {downloaded_file_path}", file=sys.stderr) + return None + + except subprocess.CalledProcessError as e: + print(f"Error running yt-dlp: {e}", file=sys.stderr) + print(f"yt-dlp Stderr: {e.stderr}", file=sys.stderr) + return None + except FileNotFoundError: + print("Error: yt-dlp not found. Please ensure yt-dlp is installed and in your PATH.", file=sys.stderr) return None except Exception as e: print(f"An unexpected error occurred during download: {e}", file=sys.stderr) return None - finally: - if temp_dir: # Clean up temporary directory if it was created - # The file might have been moved by yt-dlp, so we only clean up if it's empty - if not os.listdir(temp_dir.name): - temp_dir.cleanup() - else: - print(f"Warning: Temporary directory {temp_dir.name} not empty, not cleaning up automatically.", file=sys.stderr) - return downloaded_file_path + return downloaded_file_path \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..4f5c114 --- /dev/null +++ b/gui.py @@ -0,0 +1,207 @@ + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk +import os +import sys +import threading +import time +import converter +import downloader +import utils +import config + +class VideoConverterGUI: + def __init__(self, master): + self.master = master + master.title("Video Converter") + + self.label = tk.Label(master, text="Select video file or enter URL:") + self.label.pack() + + self.filepath_label = tk.Label(master, text="") + self.filepath_label.pack() + + self.browse_button = tk.Button(master, text="Browse", command=self.browse_file) + self.browse_button.pack() + + self.url_label = tk.Label(master, text="URL:") + self.url_label.pack() + + self.url_entry = tk.Entry(master, width=50) + self.url_entry.pack() + + self.quality_label = tk.Label(master, text="Quality:") + self.quality_label.pack() + + self.quality_var = tk.StringVar(master) + self.quality_var.set("medium") # default value + self.quality_menu = tk.OptionMenu(master, self.quality_var, "low", "medium", "high", "archive") + self.quality_menu.pack() + + self.cookies_label = tk.Label(master, text="Cookies from Browser:") + self.cookies_label.pack() + + self.cookies_var = tk.StringVar(master) + self.cookies_var.set("none") # default value + self.cookies_menu = tk.OptionMenu(master, self.cookies_var, "none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale") + self.cookies_menu.pack() + + + self.output_dir_label = tk.Label(master, text="Output Directory:") + self.output_dir_label.pack() + + self.output_dir_button = tk.Button(master, text="Select Directory", command=self.select_output_dir) + self.output_dir_button.pack() + + self.output_dir_path_label = tk.Label(master, text="") + self.output_dir_path_label.pack() + + self.convert_button = tk.Button(master, text="Convert", command=self.convert) + self.convert_button.pack() + + self.cancel_button = tk.Button(master, text="Cancel", command=self.cancel_conversion, state=tk.DISABLED) + self.cancel_button.pack() + + self.status_var = tk.StringVar() + self.status_label = tk.Label(master, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) + self.status_label.pack(side=tk.BOTTOM, fill=tk.X) + + self.progress = ttk.Progressbar(master, orient=tk.HORIZONTAL, length=100, mode='determinate') + self.progress.pack(side=tk.BOTTOM, fill=tk.X) + + def browse_file(self): + filepath = filedialog.askopenfilename() + self.filepath_label.config(text=filepath) + + def select_output_dir(self): + output_dir = filedialog.askdirectory() + self.output_dir_path_label.config(text=output_dir) + + def convert(self): + input_path = self.filepath_label.cget("text") + url = self.url_entry.get() + output_dir = self.output_dir_path_label.cget("text") + quality = self.quality_var.get() + + if not (input_path or url): + messagebox.showerror("Error", "Please select a file or enter a URL.") + return + + if not output_dir: + messagebox.showerror("Error", "Please select an output directory.") + return + + self.convert_button.config(state=tk.DISABLED) + self.cancel_button.config(state=tk.NORMAL) + self.status_var.set("Starting conversion...") + + self.conversion_thread = threading.Thread( + target=self._run_conversion, + args=(input_path, url, output_dir, quality, self.cookies_var.get()), + daemon=True + ) + self.conversion_thread.start() + + def _run_conversion(self, input_path, url, output_dir, quality, cookies_from_browser): + try: + if url: + self.status_var.set("Downloading video...") + # Since we don't have progress for downloads, we'll use indeterminate mode + self.progress['mode'] = 'indeterminate' + self.progress.start() + input_path = downloader.download_video(url, output_dir, cookies_from_browser) + self.progress.stop() + self.progress['mode'] = 'determinate' + if not input_path: + raise Exception("Download failed.") + + self.status_var.set("Converting video...") + duration = utils.get_video_duration(input_path) + if not duration: + # If we can't get the duration, use indeterminate mode + self.progress['mode'] = 'indeterminate' + self.progress.start() + else: + self.progress['maximum'] = duration + + self.progress_thread = threading.Thread(target=self._update_progress, daemon=True) + self.progress_thread.start() + + success = converter.convert_video(input_path, output_dir, quality) + + if success: + self.status_var.set("Conversion successful!") + messagebox.showinfo("Success", "Conversion completed successfully!") + else: + raise Exception("Conversion failed. Check logs for details.") + + except Exception as e: + self.status_var.set(f"Error: {e}") + try: + with open(config.FFMPEG_LOG_FILE_PATH, "r") as f: + log_lines = f.readlines() + last_10_lines = "".join(log_lines[-10:]) + messagebox.showerror("Error", f"An error occurred: {e}\n\nLast 10 lines of log:\n{last_10_lines}") + except FileNotFoundError: + messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not find log file.") + except Exception as log_e: + messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}") + + finally: + self.convert_button.config(state=tk.NORMAL) + self.cancel_button.config(state=tk.DISABLED) + self.progress.stop() + self.progress['value'] = 0 + + def _update_progress(self): + progress_file = "ffmpeg_progress.log" + while self.conversion_thread.is_alive(): + try: + with open(progress_file, "r") as f: + lines = f.readlines() + if lines: + last_line = lines[-1] + if "out_time_ms" in last_line: + time_ms = int(last_line.split("=")[1]) + self.progress['value'] = time_ms / 1000000 + except (FileNotFoundError, IndexError, ValueError): + pass + time.sleep(0.1) + + self.status_var.set("Converting video...") + success = converter.convert_video(input_path, output_dir, quality) + + if success: + self.status_var.set("Conversion successful!") + messagebox.showinfo("Success", "Conversion completed successfully!") + else: + raise Exception("Conversion failed. Check logs for details.") + + except Exception as e: + self.status_var.set(f"Error: {e}") + try: + with open(config.FFMPEG_LOG_FILE_PATH, "r") as f: + log_lines = f.readlines() + last_10_lines = "".join(log_lines[-10:]) + messagebox.showerror("Error", f"An error occurred: {e}\n\nLast 10 lines of log:\n{last_10_lines}") + except FileNotFoundError: + messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not find log file.") + except Exception as log_e: + messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}") + + finally: + self.convert_button.config(state=tk.NORMAL) + self.cancel_button.config(state=tk.DISABLED) + + def cancel_conversion(self): + # This is a bit tricky, as we can't easily kill the thread. + # For now, we'll just show a message. + # A more robust solution would involve a more complex mechanism + # to signal the thread to stop. + messagebox.showinfo("Cancel", "Cancellation requested. The current operation will continue until it is finished.") + + +if __name__ == '__main__': + root = tk.Tk() + gui = VideoConverterGUI(root) + root.mainloop() diff --git a/main.py b/main.py index 69aafe6..5f16f4b 100644 --- a/main.py +++ b/main.py @@ -1,108 +1,126 @@ -import argparse -import os -import sys +# import argparse +# import os +# import sys -# Assuming these modules will be created and populated later -import converter -import utils -import config -import downloader # Import downloader module +# # Assuming these modules will be created and populated later +# import converter +# import utils +# import config +# import downloader # Import downloader module + +# def main(): +# parser = argparse.ArgumentParser( +# description="Convert video files to DaVinci Resolve compatible formats." +# ) +# parser.add_argument( +# "input_file", +# nargs='?', +# help="Path to the input video file.", +# type=str +# ) +# parser.add_argument( +# "-u", "--url", +# help="URL of the video to download and convert.", +# type=str, +# default=None +# ) +# parser.add_argument( +# "-q", "--quality", +# help="Output video quality profile. Choices: low, medium, high, archive. (Default: medium)", +# type=str, +# choices=['low', 'medium', 'high', 'archive'], +# default='medium' +# ) +# parser.add_argument( +# "-o", "--output_dir", +# help="Optional: Directory to save the converted file. Defaults to the input file's directory.", +# type=str, +# default=None +# ) + +# args = parser.parse_args() + +# input_source = None +# if args.url: +# print(f"Downloading video from URL: {args.url}") +# downloaded_file = downloader.download_video(args.url) +# if not downloaded_file: +# print("Error: Video download failed.", file=sys.stderr) +# sys.exit(1) +# input_source = downloaded_file +# # If output_dir is not specified, use the directory of the downloaded file +# if not args.output_dir: +# args.output_dir = os.path.dirname(downloaded_file) +# elif args.input_file: +# input_source = args.input_file + +# while not input_source: +# user_input = input("Please enter the path to the input video file or a URL: ").strip() +# if not user_input: +# print("Input cannot be empty. Please try again.", file=sys.stderr) +# continue + +# if user_input.startswith("http://") or user_input.startswith("https://"): +# print(f"Downloading video from URL: {user_input}") +# downloaded_file = downloader.download_video(user_input) +# if not downloaded_file: +# print("Error: Video download failed.", file=sys.stderr) +# sys.exit(1) +# input_source = downloaded_file +# if not args.output_dir: +# args.output_dir = os.path.dirname(downloaded_file) +# else: +# input_source = user_input + +# input_file_path = os.path.abspath(input_source) + +# if not os.path.exists(input_file_path): +# print(f"Error: Input file not found at '{input_file_path}'", file=sys.stderr) +# sys.exit(1) + +# if not os.path.isfile(input_file_path): +# print(f"Error: '{input_file_path}' is not a file.", file=sys.stderr) +# sys.exit(1) + +# if not os.access(input_file_path, os.R_OK): +# print(f"Error: No read permission for input file '{input_file_path}'.", file=sys.stderr) +# sys.exit(1) + +# output_dir = args.output_dir +# if output_dir: +# output_dir = os.path.abspath(output_dir) +# if not os.path.isdir(output_dir): +# print(f"Error: Output directory '{output_dir}' does not exist or is not a directory.", file=sys.stderr) +# sys.exit(1) +# if not os.access(output_dir, os.W_OK): +# print(f"Error: No write permission for output directory '{output_dir}'.", file=sys.stderr) +# sys.exit(1) +# else: +# output_dir = os.path.dirname(input_file_path) +# if not os.access(output_dir, os.W_OK): +# print(f"Error: No write permission for input file's directory '{output_dir}'.", file=sys.stderr) +# sys.exit(1) + +# # Perform conversion +# print(f"Starting conversion for {input_file_path}...") +# success = converter.convert_video(input_file_path, output_dir, quality_level=args.quality) + +# if success: +# print("Conversion completed successfully.") +# else: +# print("Conversion failed.", file=sys.stderr) +# sys.exit(1) + +# if __name__ == "__main__": +# main() + +import tkinter as tk +from gui import VideoConverterGUI def main(): - parser = argparse.ArgumentParser( - description="Convert video files to DaVinci Resolve compatible formats." - ) - parser.add_argument( - "input_file", - nargs='?', - help="Path to the input video file.", - type=str - ) - parser.add_argument( - "-u", "--url", - help="URL of the video to download and convert.", - type=str, - default=None - ) - parser.add_argument( - "-o", "--output_dir", - help="Optional: Directory to save the converted file. Defaults to the input file's directory.", - type=str, - default=None - ) - - args = parser.parse_args() - - input_source = None - if args.url: - print(f"Downloading video from URL: {args.url}") - downloaded_file = downloader.download_video(args.url) - if not downloaded_file: - print("Error: Video download failed.", file=sys.stderr) - sys.exit(1) - input_source = downloaded_file - # If output_dir is not specified, use the directory of the downloaded file - if not args.output_dir: - args.output_dir = os.path.dirname(downloaded_file) - elif args.input_file: - input_source = args.input_file - - while not input_source: - user_input = input("Please enter the path to the input video file or a URL: ").strip() - if not user_input: - print("Input cannot be empty. Please try again.", file=sys.stderr) - continue - - if user_input.startswith("http://") or user_input.startswith("https://"): - print(f"Downloading video from URL: {user_input}") - downloaded_file = downloader.download_video(user_input) - if not downloaded_file: - print("Error: Video download failed.", file=sys.stderr) - sys.exit(1) - input_source = downloaded_file - if not args.output_dir: - args.output_dir = os.path.dirname(downloaded_file) - else: - input_source = user_input - - input_file_path = os.path.abspath(input_source) - - if not os.path.exists(input_file_path): - print(f"Error: Input file not found at '{input_file_path}'", file=sys.stderr) - sys.exit(1) - - if not os.path.isfile(input_file_path): - print(f"Error: '{input_file_path}' is not a file.", file=sys.stderr) - sys.exit(1) - - if not os.access(input_file_path, os.R_OK): - print(f"Error: No read permission for input file '{input_file_path}'.", file=sys.stderr) - sys.exit(1) - - output_dir = args.output_dir - if output_dir: - output_dir = os.path.abspath(output_dir) - if not os.path.isdir(output_dir): - print(f"Error: Output directory '{output_dir}' does not exist or is not a directory.", file=sys.stderr) - sys.exit(1) - if not os.access(output_dir, os.W_OK): - print(f"Error: No write permission for output directory '{output_dir}'.", file=sys.stderr) - sys.exit(1) - else: - output_dir = os.path.dirname(input_file_path) - if not os.access(output_dir, os.W_OK): - print(f"Error: No write permission for input file's directory '{output_dir}'.", file=sys.stderr) - sys.exit(1) - - # Perform conversion - print(f"Starting conversion for {input_file_path}...") - success = converter.convert_video(input_file_path, output_dir) - - if success: - print("Conversion completed successfully.") - else: - print("Conversion failed.", file=sys.stderr) - sys.exit(1) + root = tk.Tk() + gui = VideoConverterGUI(root) + root.mainloop() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cfbf779..b21e42f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -ffmpeg-python==0.2.0 +yt-dlp \ No newline at end of file diff --git a/src/converter.py b/src/converter.py deleted file mode 100644 index efec257..0000000 --- a/src/converter.py +++ /dev/null @@ -1,99 +0,0 @@ -import ffmpeg -import os -import sys - -from . import utils -from . import config - -def convert_video( - input_file_path, - output_dir, - video_codec=config.DEFAULT_VIDEO_CODEC, - audio_codec=config.DEFAULT_AUDIO_CODEC, - output_suffix=config.OUTPUT_SUFFIX, - output_prefix=config.OUTPUT_PREFIX, - output_ext=config.DEFAULT_CONTAINER -): - """Converts a video file to a DaVinci Resolve compatible format.""" - print(f"Analyzing input file: {input_file_path}") - video_info = utils.get_video_info(input_file_path) - audio_info = utils.get_audio_info(input_file_path) - - if not video_info or not audio_info: - print("Error: Could not retrieve full media information. Aborting conversion.", file=sys.stderr) - return False - - # Extract relevant video stream info - try: - v_stream = video_info["streams"][0] - width = v_stream["width"] - height = v_stream["height"] - # frame_rate = eval(v_stream["avg_frame_rate"]) # avg_frame_rate is a string like '30/1' - # duration = float(v_stream["duration_ts"]) / frame_rate - # pix_fmt = v_stream["pix_fmt"] - except (KeyError, IndexError) as e: - print(f"Error parsing video stream info: {e}", file=sys.stderr) - return False - - # Extract relevant audio stream info - try: - a_stream = audio_info["streams"][0] - # a_codec_name = a_stream["codec_name"] - # sample_rate = a_stream["sample_rate"] - # channels = a_stream["channels"] - except (KeyError, IndexError) as e: - print(f"Error parsing audio stream info: {e}", file=sys.stderr) - # This might be a video-only file, proceed with no audio conversion - audio_codec = None - - # Determine video profile based on resolution - video_profile = "dnxhr_sq" # Default to 1080p Standard Quality - if height > 1080: - video_profile = "dnxhr_hq" # For 1440p High Quality - - output_file_path = utils.generate_output_path( - input_file_path, output_dir, output_suffix, output_prefix, output_ext - ) - - print(f"Converting '{input_file_path}' to '{output_file_path}'") - - try: - stream = ffmpeg.input(input_file_path) - - # Video stream setup - video_stream = stream.video - video_options = { - "c:v": video_codec, - "profile:v": video_profile, - "pix_fmt": config.DEFAULT_VIDEO_PIX_FMT, - } - - # Audio stream setup - audio_stream = None - audio_options = {} - if audio_codec: - audio_stream = stream.audio - audio_options = {"c:a": audio_codec} - - # Construct output stream - if audio_stream: - out = ffmpeg.output(video_stream, audio_stream, output_file_path, **video_options, **audio_options) - else: - out = ffmpeg.output(video_stream, output_file_path, **video_options) - - # Add global options and run - out = ffmpeg.overwrite_output(out) - ffmpeg.run(out, cmd=config.FFMPEG_PATH, capture_stdout=True, capture_stderr=True) - - print(f"Successfully converted to '{output_file_path}'") - return True - - except ffmpeg.Error as e: - print(f"FFmpeg Error: {e.stderr.decode()}", file=sys.stderr) - return False - except FileNotFoundError: - print("Error: ffmpeg not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr) - return False - except Exception as e: - print(f"An unexpected error occurred during conversion: {e}", file=sys.stderr) - return False diff --git a/src/main.py b/src/main.py deleted file mode 100644 index c4107e1..0000000 --- a/src/main.py +++ /dev/null @@ -1,75 +0,0 @@ -import argparse -import os -import sys - -# Assuming these modules will be created and populated later -from . import converter -from . import utils -from . import config - -def main(): - parser = argparse.ArgumentParser( - description="Convert video files to DaVinci Resolve compatible formats." - ) - parser.add_argument( - "input_file", - nargs='?', - help="Path to the input video file.", - type=str - ) - parser.add_argument( - "-o", "--output_dir", - help="Optional: Directory to save the converted file. Defaults to the input file's directory.", - type=str, - default=None - ) - - args = parser.parse_args() - - input_file_path = args.input_file - while not input_file_path: - input_file_path = input("Please enter the path to the input video file: ").strip() - if not input_file_path: - print("Input file path cannot be empty. Please try again.", file=sys.stderr) - - input_file_path = os.path.abspath(input_file_path) - - if not os.path.exists(input_file_path): - print(f"Error: Input file not found at '{input_file_path}'", file=sys.stderr) - sys.exit(1) - - if not os.path.isfile(input_file_path): - print(f"Error: '{input_file_path}' is not a file.", file=sys.stderr) - sys.exit(1) - - if not os.access(input_file_path, os.R_OK): - print(f"Error: No read permission for input file '{input_file_path}'.", file=sys.stderr) - sys.exit(1) - - output_dir = args.output_dir - if output_dir: - output_dir = os.path.abspath(output_dir) - if not os.path.isdir(output_dir): - print(f"Error: Output directory '{output_dir}' does not exist or is not a directory.", file=sys.stderr) - sys.exit(1) - if not os.access(output_dir, os.W_OK): - print(f"Error: No write permission for output directory '{output_dir}'.", file=sys.stderr) - sys.exit(1) - else: - output_dir = os.path.dirname(input_file_path) - if not os.access(output_dir, os.W_OK): - print(f"Error: No write permission for input file's directory '{output_dir}'.", file=sys.stderr) - sys.exit(1) - - # Perform conversion - print(f"Starting conversion for {input_file_path}...") - success = converter.convert_video(input_file_path, output_dir) - - if success: - print("Conversion completed successfully.") - else: - print("Conversion failed.", file=sys.stderr) - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/src/utils.py b/utils.py similarity index 83% rename from src/utils.py rename to utils.py index d5dda81..2afdb01 100644 --- a/src/utils.py +++ b/utils.py @@ -3,7 +3,7 @@ import json import subprocess import sys -from src import config # Import config module +import config # Import config module def get_video_info(file_path): command = [ @@ -52,6 +52,21 @@ def get_audio_info(file_path): print("Error: ffprobe not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr) return None +def get_video_duration(file_path): + command = [ + config.FFPROBE_PATH, + '-v', 'error', + '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', + file_path + ] + try: + result = subprocess.run(command, capture_output=True, text=True, check=True) + return float(result.stdout) + except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e: + print(f"Error getting video duration: {e}", file=sys.stderr) + return None + def generate_output_path(input_file_path, output_dir, suffix="_DR_compatible", prefix="recoded_", new_extension=".mov"): """Generates the output file path based on input, output directory, and naming conventions.""" base_name = os.path.basename(input_file_path) diff --git a/video-converter.spec b/video-converter.spec index 81f3c78..a37678e 100644 --- a/video-converter.spec +++ b/video-converter.spec @@ -2,9 +2,9 @@ block_cipher = None -a = Analysis(['main.py', 'downloader.py'], +a = Analysis(['main.py'], pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'], - binaries=[('/usr/bin/ffmpeg', '.'), ('/usr/bin/ffprobe', '.')], + binaries=[], datas=[], hiddenimports=[], hookspath=[], @@ -28,7 +28,7 @@ exe = EXE(pyz, debug=False, strip=False, upx=True, - console=True, + console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None,