feat: Implement GUI and standalone executable

This commit is contained in:
Ramforth
2025-11-02 12:37:09 +01:00
parent 2846471d50
commit 9b0c557d9c
14 changed files with 605 additions and 492 deletions

View File

@@ -1,108 +1,36 @@
# Video Converter Script Development Plan ## Development Plan: Video Converter
## 1. Project Goal 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.
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.
## 2. Key Tools and Technologies ### Overall Goal
* **Python:** For scripting the user interface, logic, and orchestrating `ffmpeg` commands. 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.
* **`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.
**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 ### Current Priorities (Next Steps)
* **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.
### 3.2. Shell Environment (Re-evaluation) 1. **Implement GUI:** Develop a graphical user interface for easier interaction, including file selection, quality profile choice, and URL input. (High Priority)
* 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. 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) ### Known Issues / Limitations
* **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.
#### Audio Codecs * DaVinci Resolve Free on Linux has limitations with certain audio codecs (e.g., AAC) and 4K output/encoding (requires Studio version).
* **PCM (Pulse Code Modulation):** * `ffmpeg-full` AUR package is required for `dnxhd` support on Arch-based systems.
* **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.

View File

@@ -1,6 +1,12 @@
# Video Converter for DaVinci Resolve # 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. 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.

102
USAGE.md
View File

@@ -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 ## 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. * **`ffmpeg-full`:** A version of `ffmpeg` compiled with DNxHD/HR support.
* Install from AUR: `yay -S ffmpeg-full` (or your preferred AUR helper). * 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. * **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 ## 2. Setup
1. **Clone the Repository:** 1. **Download the application:**
```bash You can either clone the repository and build the application yourself, or download the latest release from the project's Gitea page.
git clone https://gitea.ramforth.net/ramforth/video-converter.git
cd video-converter
```
2. **Create and Activate a Python Virtual Environment:** 2. **Building from source (optional):**
It's highly recommended to use a virtual environment to manage Python dependencies. If you have cloned the repository, you can build the executable by running the following commands:
```bash ```bash
python3 -m venv venv # Activate the virtual environment
source venv/bin/activate source venv/bin/activate
# Build the executable
pyinstaller --noconfirm video-converter.spec
``` ```
3. **Install Python Dependencies:** ## 3. Running the Application
```bash
pip install -r requirements.txt
```
## 3. Running the Converter Once you have the executable, you can run it from your terminal.
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 <video_url>] [-q <quality_profile>] [-o <path_to_output_directory>]
```
* 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 <video_url>` 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 <quality_profile>` 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 `<path_to_output_directory>` 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.
1. **Navigate to the `dist` directory:** 1. **Navigate to the `dist` directory:**
```bash ```bash
cd /path/to/your/project/video-converter/dist cd /path/to/your/project/video-converter/dist
``` ```
2. **Run the executable:**
2. **Make the executable runnable (if necessary):**
```bash ```bash
./video-converter [path_to_your_input_video_file] [-u <video_url>] [-q <quality_profile>] [-o <path_to_output_directory>] chmod +x video-converter
``` ```
The usage is identical to the Python script, but you execute the binary directly. 3. **Run the executable:**
**Example:**
```bash ```bash
./video-converter /home/user/Videos/my_original_video.mp4 -q low -o /home/user/ConvertedVideos ./video-converter
./video-converter -u "https://www.youtube.com/watch?v=dQw4w9WgXcQ" -q archive
``` ```
## 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) The GUI provides the following options:
* **1080p (<=1080 height):** `dnxhr_sq` (Standard Quality)
* **1440p (>1080 height):** `dnxhr_hq` (High Quality)
* **Audio Codec:** `pcm_s16le` (Linear PCM)
* **Container:** `.mov` (QuickTime)
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.
--- ## 5. Logging and Error Reporting
*Document generated by Gemini CLI Agent.*
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.

View File

@@ -6,8 +6,29 @@ import os
# Target 1080p60 and 1440p60 # Target 1080p60 and 1440p60
DEFAULT_VIDEO_CODEC = "dnxhd" 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_VIDEO_PIX_FMT = "yuv422p" # Common for DNxHD/HR
DEFAULT_CONTAINER = ".mov" DEFAULT_CONTAINER = ".mov"
@@ -28,6 +49,7 @@ FFPROBE_GLOBAL_OPTIONS = [
# For development, assume they are in PATH # For development, assume they are in PATH
FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "ffmpeg") FFMPEG_PATH = os.environ.get("FFMPEG_PATH", "ffmpeg")
FFPROBE_PATH = os.environ.get("FFPROBE_PATH", "ffprobe") FFPROBE_PATH = os.environ.get("FFPROBE_PATH", "ffprobe")
FFMPEG_LOG_FILE_PATH = "ffmpeg_output.log"
# Output file naming conventions # Output file naming conventions
OUTPUT_SUFFIX = "_DR_compatible" OUTPUT_SUFFIX = "_DR_compatible"

108
converter.py Normal file
View File

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

View File

@@ -1,9 +1,11 @@
import yt_dlp
import os import os
import sys import sys
import tempfile 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. """Downloads a video from the given URL to a temporary location or specified output directory.
Args: Args:
@@ -13,59 +15,66 @@ def download_video(url, output_dir=None):
Returns: Returns:
str: The absolute path to the downloaded video file, or None if download fails. 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) print(f"Error: Output directory '{output_dir}' for download does not exist or is not a directory.", file=sys.stderr)
return None return None
temp_dir = None # Construct yt-dlp command
if not output_dir: download_filename = "yt_dlp_downloaded_video.mp4"
temp_dir = tempfile.TemporaryDirectory() # Create a temporary directory yt_dlp_command = [
output_dir = temp_dir.name "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 = { if cookies_from_browser and cookies_from_browser != "none":
'format': 'bestvideo[ext!=webm]+bestaudio[ext!=webm]/best[ext!=webm]', # Prioritize non-webm formats yt_dlp_command.extend(["--cookies-from-browser", cookies_from_browser])
'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,
}
downloaded_file_path = None downloaded_file_path = None
try: try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # print(f"Executing yt-dlp command: {' '.join(yt_dlp_command)}", file=sys.stderr)
info_dict = ydl.extract_info(url, download=True) process = subprocess.run(
downloaded_file_path = ydl.prepare_filename(info_dict) yt_dlp_command,
# yt-dlp might return a path with a different extension if format conversion happened capture_output=True,
# We need to find the actual downloaded file text=True,
base, ext = os.path.splitext(downloaded_file_path) check=True
if not os.path.exists(downloaded_file_path): )
# Try common extensions if the exact path isn't found # print(f"DEBUG: yt-dlp stdout: {process.stdout}", file=sys.stderr)
for common_ext in ['.mp4', '.mkv', '.webm', '.flv']: # print(f"DEBUG: yt-dlp stderr: {process.stderr}", file=sys.stderr)
if os.path.exists(base + common_ext):
downloaded_file_path = base + common_ext
break
if not os.path.exists(downloaded_file_path): # The downloaded file path is now explicitly set
print(f"Error: Downloaded file not found at expected path: {downloaded_file_path}", file=sys.stderr) downloaded_file_path = os.path.join(output_dir, download_filename)
return None
except yt_dlp.utils.DownloadError as e: # print(f"DEBUG: Parsed downloaded_file_path: {downloaded_file_path}", file=sys.stderr)
print(f"Error downloading video: {e}", 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 return None
except Exception as e: except Exception as e:
print(f"An unexpected error occurred during download: {e}", file=sys.stderr) print(f"An unexpected error occurred during download: {e}", file=sys.stderr)
return None 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

207
gui.py Normal file
View File

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

222
main.py
View File

@@ -1,108 +1,126 @@
import argparse # import argparse
import os # import os
import sys # import sys
# Assuming these modules will be created and populated later # # Assuming these modules will be created and populated later
import converter # import converter
import utils # import utils
import config # import config
import downloader # Import downloader module # 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(): def main():
parser = argparse.ArgumentParser( root = tk.Tk()
description="Convert video files to DaVinci Resolve compatible formats." gui = VideoConverterGUI(root)
) root.mainloop()
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)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1 +1 @@
ffmpeg-python==0.2.0 yt-dlp

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import json
import subprocess import subprocess
import sys import sys
from src import config # Import config module import config # Import config module
def get_video_info(file_path): def get_video_info(file_path):
command = [ 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) print("Error: ffprobe not found. Please ensure FFmpeg is installed and in your PATH.", file=sys.stderr)
return None 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"): 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.""" """Generates the output file path based on input, output directory, and naming conventions."""
base_name = os.path.basename(input_file_path) base_name = os.path.basename(input_file_path)

View File

@@ -2,9 +2,9 @@
block_cipher = None block_cipher = None
a = Analysis(['main.py', 'downloader.py'], a = Analysis(['main.py'],
pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'], pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'],
binaries=[('/usr/bin/ffmpeg', '.'), ('/usr/bin/ffprobe', '.')], binaries=[],
datas=[], datas=[],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
@@ -28,7 +28,7 @@ exe = EXE(pyz,
debug=False, debug=False,
strip=False, strip=False,
upx=True, upx=True,
console=True, console=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,