Compare commits
2 Commits
01e3553796
...
15e5933c05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e5933c05 | ||
|
|
9b0c557d9c |
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
102
USAGE.md
102
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 <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.
|
||||
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 <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.
|
||||
|
||||
**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.*
|
||||
## 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.
|
||||
@@ -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"
|
||||
108
converter.py
Normal file
108
converter.py
Normal 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
|
||||
@@ -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
|
||||
207
gui.py
Normal file
207
gui.py
Normal 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()
|
||||
224
main.py
224
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()
|
||||
@@ -1 +1 @@
|
||||
ffmpeg-python==0.2.0
|
||||
yt-dlp
|
||||
@@ -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
|
||||
75
src/main.py
75
src/main.py
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user