Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dffc6a2bbc | ||
|
|
5ad7eb2ea1 | ||
|
|
5f1363f335 | ||
|
|
0b1816a310 | ||
|
|
d62742ef00 | ||
|
|
841bbac9d4 | ||
|
|
b4815a4a19 | ||
|
|
7283966b2e | ||
|
|
5ee4b3e8f3 | ||
|
|
603ba2f679 | ||
|
|
46bb80338a | ||
|
|
2f773657a1 | ||
|
|
dd37b38498 | ||
|
|
3b4890c8e8 | ||
|
|
55a3838be7 | ||
|
|
a791f4d8ca | ||
|
|
4f61d86e2a | ||
|
|
710f77f403 | ||
|
|
b4ffdfa977 | ||
|
|
1250afc9e5 | ||
|
|
e0fb81197c | ||
|
|
0a49866662 |
@@ -43,3 +43,25 @@ Develop a user-friendly, standalone Python tool to convert video files into form
|
|||||||
Since one of the limitations of this project is the requirement of ffmpeg-full for encoding videos for Davinci Resolve use we should investigate the proposition of implementing the necessary ffmpeg-full into the project release file.
|
Since one of the limitations of this project is the requirement of ffmpeg-full for encoding videos for Davinci Resolve use we should investigate the proposition of implementing the necessary ffmpeg-full into the project release file.
|
||||||
I do not know, currently if this is viable or even possible. But I think that a fully precompiled program with built in encoders and tools would be more attractive to most users.
|
I do not know, currently if this is viable or even possible. But I think that a fully precompiled program with built in encoders and tools would be more attractive to most users.
|
||||||
We should also see if there are other tools that would be considered natural for this project.
|
We should also see if there are other tools that would be considered natural for this project.
|
||||||
|
|
||||||
|
**Assessment Report: Bundling FFmpeg and Other Tools**
|
||||||
|
|
||||||
|
**1. Bundling `ffmpeg-full`:**
|
||||||
|
* **Viability:** Technically viable and possible. PyInstaller (and similar tools) can include external binaries like `ffmpeg` in the final executable. The Python script would then call this bundled binary.
|
||||||
|
* **User Attractiveness:** Highly attractive. A self-contained executable eliminates the need for users to manually install `ffmpeg`, especially specific versions like `ffmpeg-full` with custom codecs, significantly improving user experience and simplifying setup.
|
||||||
|
* **Challenges:**
|
||||||
|
* **Cross-Platform Complexity:** Requires separate `ffmpeg` binaries for each target OS (Linux, Windows, macOS), increasing build complexity.
|
||||||
|
* **Licensing:** Must comply with `ffmpeg`'s LGPLv2.1/GPLv2 licensing terms (e.g., providing source code).
|
||||||
|
* **Maintenance:** Keeping bundled `ffmpeg` updated for security and features requires re-bundling.
|
||||||
|
* **Executable Size:** Bundling `ffmpeg` will increase the overall size of the application executable.
|
||||||
|
|
||||||
|
**2. Other Natural Tools for Consideration:**
|
||||||
|
* **`ffprobe`:** (Already implicitly used) Part of the `ffmpeg` suite, essential for media analysis (e.g., video duration). Would be bundled alongside `ffmpeg`.
|
||||||
|
* **`mediainfo`:** A powerful command-line tool for detailed media file information. Could be integrated for advanced diagnostics or user-facing metadata display.
|
||||||
|
* **`exiftool`:** For comprehensive metadata handling. Relevant if "Metadata Preservation" becomes a core feature.
|
||||||
|
|
||||||
|
### Questions for Challenges with bundling
|
||||||
|
* Cross platform development is currently delayed until after final stages of Linux project.
|
||||||
|
Reason is, among other things, that Windows do not see the same restrictions as Linux users meet in this actual field. So converting video files in this manner is mostly a Linux theme.
|
||||||
|
* Licensing: Will the entire source code have to be provided along side the downloadable package - or can it remain available as a Gitea repository for reference?
|
||||||
|
* Size of executable: We will have to test numerous iterations to lower the size of the finished product. However, I am also confident that Linux users that face the challenge of editing videos and recoding videos at a semi-regular rate will have enough storage space to handle a sizeable application if it solves the actual problem.
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ For instructions on how to use the application, please refer to the [HOW-TO.md](
|
|||||||
|
|
||||||
The application's graphical user interface has been updated to use `customtkinter` as of November 2, 2025. This provides a modern and customizable look and feel.
|
The application's graphical user interface has been updated to use `customtkinter` as of November 2, 2025. This provides a modern and customizable look and feel.
|
||||||
|
|
||||||
<!-- Screenshot of the GUI will be placed here -->
|
<p align="center">
|
||||||
|
<img src="images/video_converter.png" alt="Video Converter GUI" style="display: block; margin: 0 auto;">
|
||||||
|
</p>
|
||||||
|
|
||||||
## Setup Notes for Arch Linux / CachyOS
|
## Setup Notes for Arch Linux / CachyOS
|
||||||
|
|
||||||
|
|||||||
56
custom_theme_dark.json
Normal file
56
custom_theme_dark.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"CTk": {
|
||||||
|
"fg_color": ["#121212", "#121212"]
|
||||||
|
},
|
||||||
|
"CTkFrame": {
|
||||||
|
"fg_color": ["#121212", "#121212"],
|
||||||
|
"border_color": ["#333333", "#333333"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_width": 0
|
||||||
|
},
|
||||||
|
"CTkLabel": {
|
||||||
|
"text_color": ["#ecd7b2", "#ecd7b2"],
|
||||||
|
"fg_color": "transparent",
|
||||||
|
"corner_radius": 0
|
||||||
|
},
|
||||||
|
"CTkButton": {
|
||||||
|
"fg_color": ["#21498a", "#21498a"],
|
||||||
|
"hover_color": ["#2a5c9e", "#2a5c9e"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#333333", "#333333"]
|
||||||
|
},
|
||||||
|
"CTkEntry": {
|
||||||
|
"fg_color": ["#1e1e1e", "#1e1e1e"],
|
||||||
|
"text_color": ["#ecd7b2", "#ecd7b2"],
|
||||||
|
"placeholder_text_color": ["#888888", "#888888"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_width": 2,
|
||||||
|
"border_color": ["#333333", "#333333"]
|
||||||
|
},
|
||||||
|
"CTkOptionMenu": {
|
||||||
|
"fg_color": ["#21498a", "#21498a"],
|
||||||
|
"button_color": ["#21498a", "#21498a"],
|
||||||
|
"button_hover_color": ["#2a5c9e", "#2a5c9e"],
|
||||||
|
"text_color": ["#ecd7b2", "#ecd7b2"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#333333", "#333333"]
|
||||||
|
},
|
||||||
|
"CTkProgressBar": {
|
||||||
|
"fg_color": ["#1e1e1e", "#1e1e1e"],
|
||||||
|
"progress_color": ["#21498a", "#21498a"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#333333", "#333333"]
|
||||||
|
},
|
||||||
|
"CTkSwitch": {
|
||||||
|
"fg_color": ["#1e1e1e", "#1e1e1e"],
|
||||||
|
"progress_color": ["#21498a", "#21498a"],
|
||||||
|
"text_color": ["#ecd7b2", "#ecd7b2"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#333333", "#333333"]
|
||||||
|
},
|
||||||
|
"CTkFont": {
|
||||||
|
"family": ["Roboto", "Roboto"],
|
||||||
|
"size": [13, 13],
|
||||||
|
"weight": ["normal", "normal"]
|
||||||
|
}
|
||||||
|
}
|
||||||
56
custom_theme_light.json
Normal file
56
custom_theme_light.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"CTk": {
|
||||||
|
"fg_color": ["#ecd7b2", "#ecd7b2"]
|
||||||
|
},
|
||||||
|
"CTkFrame": {
|
||||||
|
"fg_color": ["#ecd7b2", "#ecd7b2"],
|
||||||
|
"border_color": ["#cccccc", "#cccccc"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_width": 0
|
||||||
|
},
|
||||||
|
"CTkLabel": {
|
||||||
|
"text_color": ["#000000", "#000000"],
|
||||||
|
"fg_color": "transparent",
|
||||||
|
"corner_radius": 0
|
||||||
|
},
|
||||||
|
"CTkButton": {
|
||||||
|
"fg_color": ["#4077d1", "#4077d1"],
|
||||||
|
"hover_color": ["#5a8ee6", "#5a8ee6"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#cccccc", "#cccccc"]
|
||||||
|
},
|
||||||
|
"CTkEntry": {
|
||||||
|
"fg_color": ["#ffffff", "#ffffff"],
|
||||||
|
"text_color": ["#000000", "#000000"],
|
||||||
|
"placeholder_text_color": ["#888888", "#888888"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_width": 2,
|
||||||
|
"border_color": ["#cccccc", "#cccccc"]
|
||||||
|
},
|
||||||
|
"CTkOptionMenu": {
|
||||||
|
"fg_color": ["#4077d1", "#4077d1"],
|
||||||
|
"button_color": ["#4077d1", "#4077d1"],
|
||||||
|
"button_hover_color": ["#5a8ee6", "#5a8ee6"],
|
||||||
|
"text_color": ["#000000", "#000000"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#cccccc", "#cccccc"]
|
||||||
|
},
|
||||||
|
"CTkProgressBar": {
|
||||||
|
"fg_color": ["#ffffff", "#ffffff"],
|
||||||
|
"progress_color": ["#4077d1", "#4077d1"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#cccccc", "#cccccc"]
|
||||||
|
},
|
||||||
|
"CTkSwitch": {
|
||||||
|
"fg_color": ["#ffffff", "#ffffff"],
|
||||||
|
"progress_color": ["#4077d1", "#4077d1"],
|
||||||
|
"text_color": ["#000000", "#000000"],
|
||||||
|
"corner_radius": 8,
|
||||||
|
"border_color": ["#cccccc", "#cccccc"]
|
||||||
|
},
|
||||||
|
"CTkFont": {
|
||||||
|
"family": ["Roboto", "Roboto"],
|
||||||
|
"size": [13, 13],
|
||||||
|
"weight": ["normal", "normal"]
|
||||||
|
}
|
||||||
|
}
|
||||||
205
gui.py
205
gui.py
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
import customtkinter
|
import customtkinter
|
||||||
from tkinter import filedialog, messagebox # Keep these for standard dialogs
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import converter
|
import converter
|
||||||
|
import webbrowser
|
||||||
import downloader
|
import downloader
|
||||||
import utils
|
import utils
|
||||||
import config
|
import config
|
||||||
@@ -14,71 +15,129 @@ class VideoConverterGUI:
|
|||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
master.title("Video Converter")
|
master.title("Video Converter")
|
||||||
|
master.geometry("600x750+100+100")
|
||||||
|
|
||||||
customtkinter.set_appearance_mode("System") # Modes: "System" (default), "Dark", "Light"
|
# --- Set Dark Mode by Default ---
|
||||||
customtkinter.set_default_color_theme("blue") # Themes: "blue" (default), "green", "dark-blue")
|
customtkinter.set_appearance_mode("Dark")
|
||||||
|
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme for build stability
|
||||||
|
|
||||||
self.label = customtkinter.CTkLabel(master, text="Select video file or enter URL:")
|
# Create the main content frame
|
||||||
self.label.pack()
|
self.main_frame = customtkinter.CTkFrame(master, fg_color="transparent") # Make it transparent to root
|
||||||
|
self.main_frame.pack(fill="both", expand=True) # Add some padding to the main frame
|
||||||
|
|
||||||
self.filepath_label = customtkinter.CTkLabel(master, text="")
|
# --- Footer Frame (packed into main_frame) ---
|
||||||
self.filepath_label.pack()
|
self.footer_frame = customtkinter.CTkFrame(self.main_frame, fg_color="transparent")
|
||||||
|
self.footer_frame.pack(side="bottom", fill="x", pady=(5, 10))
|
||||||
|
|
||||||
self.browse_button = customtkinter.CTkButton(master, text="Browse", command=self.browse_file)
|
self.nickname_label = customtkinter.CTkLabel(self.footer_frame, text="by ramforth")
|
||||||
self.browse_button.pack()
|
self.nickname_label.pack(side="left", padx=(10, 0))
|
||||||
|
|
||||||
self.url_label = customtkinter.CTkLabel(master, text="URL:")
|
self.gitea_link = customtkinter.CTkLabel(self.footer_frame, text="https://gitea.ramforth.net/ramforth", text_color="#4077d1", cursor="hand2")
|
||||||
self.url_label.pack()
|
self.gitea_link.pack(side="right", padx=(0, 10))
|
||||||
|
self.gitea_link.bind("<Button-1>", lambda e: self.open_link("https://gitea.ramforth.net/ramforth"))
|
||||||
|
underline_font = customtkinter.CTkFont(family="sans-serif", size=12, underline=True)
|
||||||
|
self.gitea_link.configure(font=underline_font)
|
||||||
|
|
||||||
self.url_entry = customtkinter.CTkEntry(master, width=50)
|
# --- Status and Progress (packed into main_frame) ---
|
||||||
self.url_entry.pack()
|
self.status_var = customtkinter.StringVar(value="Ready")
|
||||||
|
self.status_label = customtkinter.CTkLabel(self.main_frame, textvariable=self.status_var)
|
||||||
|
self.status_label.pack(side="bottom", fill="x", padx=10, pady=(5, 0))
|
||||||
|
|
||||||
self.quality_label = customtkinter.CTkLabel(master, text="Quality:")
|
self.progress = customtkinter.CTkProgressBar(self.main_frame, orientation="horizontal")
|
||||||
self.quality_label.pack()
|
self.progress.pack(side="bottom", fill="x", padx=10, pady=(0, 10))
|
||||||
|
self.progress.set(0)
|
||||||
|
|
||||||
self.quality_var = customtkinter.StringVar(master)
|
# --- Mode Switch (packed into main_frame) ---
|
||||||
self.quality_var.set("medium") # default value
|
self.mode_switch = customtkinter.CTkSwitch(self.main_frame, text="", command=self.toggle_mode)
|
||||||
self.quality_menu = customtkinter.CTkOptionMenu(master, variable=self.quality_var, values=["low", "medium", "high", "archive"])
|
self.mode_switch.pack(pady=10, padx=20, anchor="w")
|
||||||
self.quality_menu.pack()
|
self.mode_switch.select()
|
||||||
|
|
||||||
self.cookies_label = customtkinter.CTkLabel(master, text="Cookies from Browser:")
|
self.mode_label = customtkinter.CTkLabel(self.main_frame)
|
||||||
self.cookies_label.pack()
|
self.mode_label.pack(pady=0, padx=20, anchor="w")
|
||||||
|
|
||||||
self.cookies_var = customtkinter.StringVar(master)
|
self.toggle_mode() # Ensure initial mode is set based on switch state
|
||||||
self.cookies_var.set("none") # default value
|
|
||||||
self.cookies_menu = customtkinter.CTkOptionMenu(master, variable=self.cookies_var, values=["none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale"])
|
|
||||||
self.cookies_menu.pack()
|
|
||||||
|
|
||||||
|
# --- Main Widgets (packed into main_frame) ---
|
||||||
|
self.input_label_frame = customtkinter.CTkFrame(self.main_frame, fg_color="transparent")
|
||||||
|
self.input_label_frame.pack(pady=(10, 5))
|
||||||
|
|
||||||
self.output_dir_label = customtkinter.CTkLabel(master, text="Output Directory:")
|
self.label_part1 = customtkinter.CTkLabel(self.input_label_frame, text="Select video file ")
|
||||||
self.output_dir_label.pack()
|
self.label_part1.pack(side="left")
|
||||||
|
|
||||||
self.output_dir_button = customtkinter.CTkButton(master, text="Select Directory", command=self.select_output_dir)
|
or_font = customtkinter.CTkFont(family="sans-serif", weight="bold", underline=True)
|
||||||
self.output_dir_button.pack()
|
self.label_or = customtkinter.CTkLabel(self.input_label_frame, text="or", font=or_font)
|
||||||
|
self.label_or.pack(side="left")
|
||||||
|
|
||||||
self.output_dir_path_label = customtkinter.CTkLabel(master, text="")
|
self.label_part2 = customtkinter.CTkLabel(self.input_label_frame, text=" enter URL:")
|
||||||
self.output_dir_path_label.pack()
|
self.label_part2.pack(side="left")
|
||||||
|
|
||||||
self.convert_button = customtkinter.CTkButton(master, text="Convert", command=self.convert)
|
self.filepath_label = customtkinter.CTkLabel(self.main_frame, text="No file selected", fg_color=("gray75", "gray25"))
|
||||||
self.convert_button.pack()
|
self.filepath_label.pack(pady=5, padx=20, fill="x")
|
||||||
|
|
||||||
self.cancel_button = customtkinter.CTkButton(master, text="Cancel", command=self.cancel_conversion, state=customtkinter.DISABLED)
|
self.browse_button = customtkinter.CTkButton(self.main_frame, text="Browse", command=self.browse_file)
|
||||||
self.cancel_button.pack()
|
self.browse_button.pack(pady=5)
|
||||||
|
|
||||||
self.status_var = customtkinter.StringVar()
|
self.url_label = customtkinter.CTkLabel(self.main_frame, text="URL:")
|
||||||
self.status_label = customtkinter.CTkLabel(master, textvariable=self.status_var)
|
self.url_label.pack(pady=(15, 5))
|
||||||
self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
|
|
||||||
|
|
||||||
self.progress = customtkinter.CTkProgressBar(master, orientation="horizontal")
|
self.url_entry = customtkinter.CTkEntry(self.main_frame, placeholder_text="https://...")
|
||||||
self.progress.pack(side=tk.BOTTOM, fill=tk.X)
|
self.url_entry.pack(pady=5, padx=20, fill="x")
|
||||||
|
|
||||||
|
self.quality_label = customtkinter.CTkLabel(self.main_frame, text="Quality:", fg_color="transparent")
|
||||||
|
self.quality_label.pack(pady=(15, 5))
|
||||||
|
|
||||||
|
self.quality_var = customtkinter.StringVar(master, value="medium")
|
||||||
|
self.quality_menu = customtkinter.CTkOptionMenu(self.main_frame, variable=self.quality_var, values=["low", "medium", "high", "archive"])
|
||||||
|
self.quality_menu.pack(pady=5)
|
||||||
|
|
||||||
|
self.cookies_label = customtkinter.CTkLabel(self.main_frame, text="Cookies from Browser:", fg_color="transparent")
|
||||||
|
self.cookies_label.pack(pady=(15, 5))
|
||||||
|
|
||||||
|
self.cookies_var = customtkinter.StringVar(master, value="none")
|
||||||
|
self.cookies_menu = customtkinter.CTkOptionMenu(self.main_frame, variable=self.cookies_var, values=["none", "brave", "chrome", "chromium", "edge", "firefox", "opera", "safari", "vivaldi", "whale"])
|
||||||
|
self.cookies_menu.pack(pady=5)
|
||||||
|
|
||||||
|
self.output_dir_label = customtkinter.CTkLabel(self.main_frame, text="Output Directory:", fg_color="transparent")
|
||||||
|
self.output_dir_label.pack(pady=(15, 5))
|
||||||
|
|
||||||
|
self.output_dir_button = customtkinter.CTkButton(self.main_frame, text="Select Directory", command=self.select_output_dir)
|
||||||
|
self.output_dir_button.pack(pady=5)
|
||||||
|
|
||||||
|
self.output_dir_path_label = customtkinter.CTkLabel(self.main_frame, text="No directory selected", fg_color=("gray75", "gray25"))
|
||||||
|
self.output_dir_path_label.pack(pady=5, padx=20, fill="x")
|
||||||
|
|
||||||
|
self.convert_button = customtkinter.CTkButton(self.main_frame, text="Convert", command=self.convert)
|
||||||
|
self.convert_button.pack(pady=(20, 5))
|
||||||
|
|
||||||
|
self.cancel_button = customtkinter.CTkButton(self.main_frame, text="Cancel", command=self.cancel_conversion, state="disabled")
|
||||||
|
self.cancel_button.pack(pady=5)
|
||||||
|
|
||||||
|
def open_link(self, url):
|
||||||
|
"""Opens the given URL in a web browser."""
|
||||||
|
webbrowser.open_new(url)
|
||||||
|
|
||||||
|
def toggle_mode(self):
|
||||||
|
# Check if the switch is on (1) or off (0)
|
||||||
|
if self.mode_switch.get() == 1:
|
||||||
|
customtkinter.set_appearance_mode("Dark")
|
||||||
|
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme
|
||||||
|
self.main_frame.configure(fg_color="#121212") # Set main frame background
|
||||||
|
self.mode_label.configure(text="Dark Mode")
|
||||||
|
else:
|
||||||
|
customtkinter.set_appearance_mode("Light")
|
||||||
|
customtkinter.set_default_color_theme("blue") # Temporarily using built-in theme
|
||||||
|
self.main_frame.configure(fg_color="#ecd7b2") # Set main frame background
|
||||||
|
self.mode_label.configure(text="Light Mode")
|
||||||
|
|
||||||
def browse_file(self):
|
def browse_file(self):
|
||||||
filepath = filedialog.askopenfilename()
|
filepath = filedialog.askopenfilename()
|
||||||
self.filepath_label.config(text=filepath)
|
if filepath:
|
||||||
|
self.filepath_label.configure(text=filepath)
|
||||||
|
|
||||||
def select_output_dir(self):
|
def select_output_dir(self):
|
||||||
output_dir = filedialog.askdirectory()
|
output_dir = filedialog.askdirectory()
|
||||||
self.output_dir_path_label.config(text=output_dir)
|
if output_dir:
|
||||||
|
self.output_dir_path_label.configure(text=output_dir)
|
||||||
|
|
||||||
def convert(self):
|
def convert(self):
|
||||||
input_path = self.filepath_label.cget("text")
|
input_path = self.filepath_label.cget("text")
|
||||||
@@ -86,17 +145,23 @@ class VideoConverterGUI:
|
|||||||
output_dir = self.output_dir_path_label.cget("text")
|
output_dir = self.output_dir_path_label.cget("text")
|
||||||
quality = self.quality_var.get()
|
quality = self.quality_var.get()
|
||||||
|
|
||||||
|
if url:
|
||||||
|
input_path = ""
|
||||||
|
self.filepath_label.configure(text="Using URL")
|
||||||
|
elif input_path == "No file selected":
|
||||||
|
input_path = ""
|
||||||
|
|
||||||
if not (input_path or url):
|
if not (input_path or url):
|
||||||
messagebox.showerror("Error", "Please select a file or enter a URL.")
|
messagebox.showerror("Error", "Please select a file or enter a URL.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not output_dir:
|
if output_dir == "No directory selected":
|
||||||
messagebox.showerror("Error", "Please select an output directory.")
|
messagebox.showerror("Error", "Please select an output directory.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.convert_button.config(state=tk.DISABLED)
|
self.convert_button.configure(state="disabled")
|
||||||
self.cancel_button.config(state=tk.NORMAL)
|
self.cancel_button.configure(state="normal")
|
||||||
self.status_var.set("Starting conversion...")
|
self.status_var.set("Starting...")
|
||||||
|
|
||||||
self.conversion_thread = threading.Thread(
|
self.conversion_thread = threading.Thread(
|
||||||
target=self._run_conversion,
|
target=self._run_conversion,
|
||||||
@@ -109,31 +174,32 @@ class VideoConverterGUI:
|
|||||||
try:
|
try:
|
||||||
if url:
|
if url:
|
||||||
self.status_var.set("Downloading video...")
|
self.status_var.set("Downloading video...")
|
||||||
# Since we don't have progress for downloads, we'll use indeterminate mode
|
self.progress.configure(mode='indeterminate')
|
||||||
self.progress['mode'] = 'indeterminate'
|
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
input_path = downloader.download_video(url, output_dir, cookies_from_browser)
|
input_path = downloader.download_video(url, output_dir, cookies_from_browser)
|
||||||
self.progress.stop()
|
self.progress.stop()
|
||||||
self.progress['mode'] = 'determinate'
|
self.progress.configure(mode='determinate')
|
||||||
if not input_path:
|
if not input_path:
|
||||||
raise Exception("Download failed.")
|
raise Exception("Download failed.")
|
||||||
|
|
||||||
self.status_var.set("Converting video...")
|
self.status_var.set("Converting video...")
|
||||||
duration = utils.get_video_duration(input_path)
|
duration = utils.get_video_duration(input_path)
|
||||||
|
|
||||||
if not duration:
|
if not duration:
|
||||||
# If we can't get the duration, use indeterminate mode
|
self.progress.configure(mode='indeterminate')
|
||||||
self.progress['mode'] = 'indeterminate'
|
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
else:
|
else:
|
||||||
self.progress['maximum'] = duration
|
self.progress.configure(mode='determinate')
|
||||||
|
self.progress.set(0)
|
||||||
|
|
||||||
self.progress_thread = threading.Thread(target=self._update_progress, daemon=True)
|
self.progress_thread = threading.Thread(target=self._update_progress, args=(duration,), daemon=True)
|
||||||
self.progress_thread.start()
|
self.progress_thread.start()
|
||||||
|
|
||||||
success = converter.convert_video(input_path, output_dir, quality)
|
success = converter.convert_video(input_path, output_dir, quality)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.status_var.set("Conversion successful!")
|
self.status_var.set("Conversion successful!")
|
||||||
|
self.progress.set(1)
|
||||||
messagebox.showinfo("Success", "Conversion completed successfully!")
|
messagebox.showinfo("Success", "Conversion completed successfully!")
|
||||||
else:
|
else:
|
||||||
raise Exception("Conversion failed. Check logs for details.")
|
raise Exception("Conversion failed. Check logs for details.")
|
||||||
@@ -151,12 +217,17 @@ class VideoConverterGUI:
|
|||||||
messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}")
|
messagebox.showerror("Error", f"An error occurred: {e}\n\nCould not read log file: {log_e}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.convert_button.config(state=tk.NORMAL)
|
self.convert_button.configure(state="normal")
|
||||||
self.cancel_button.config(state=tk.DISABLED)
|
self.cancel_button.configure(state="disabled")
|
||||||
self.progress.stop()
|
self.progress.stop()
|
||||||
self.progress['value'] = 0
|
if "Error" not in self.status_var.get():
|
||||||
|
self.progress.set(1)
|
||||||
|
else:
|
||||||
|
self.progress.set(0)
|
||||||
|
if self.status_var.get() == "Starting...":
|
||||||
|
self.status_var.set("Ready")
|
||||||
|
|
||||||
def _update_progress(self):
|
def _update_progress(self, duration):
|
||||||
progress_file = "ffmpeg_progress.log"
|
progress_file = "ffmpeg_progress.log"
|
||||||
while self.conversion_thread.is_alive():
|
while self.conversion_thread.is_alive():
|
||||||
try:
|
try:
|
||||||
@@ -166,20 +237,22 @@ class VideoConverterGUI:
|
|||||||
last_line = lines[-1]
|
last_line = lines[-1]
|
||||||
if "out_time_ms" in last_line:
|
if "out_time_ms" in last_line:
|
||||||
time_ms = int(last_line.split("=")[1])
|
time_ms = int(last_line.split("=")[1])
|
||||||
self.progress['value'] = time_ms / 1000000
|
current_time_sec = time_ms / 1000000
|
||||||
|
if duration and duration > 0:
|
||||||
|
progress_value = max(0, min(1, current_time_sec / duration))
|
||||||
|
self.progress.set(progress_value)
|
||||||
except (FileNotFoundError, IndexError, ValueError):
|
except (FileNotFoundError, IndexError, ValueError):
|
||||||
pass
|
pass
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def cancel_conversion(self):
|
def cancel_conversion(self):
|
||||||
# This is a bit tricky, as we can't easily kill the thread.
|
self.status_var.set("Cancellation requested...")
|
||||||
# For now, we'll just show a message.
|
messagebox.showinfo("Cancel", "Cancellation requested. The current operation will try to stop.")
|
||||||
# A more robust solution would involve a more complex mechanism
|
self.convert_button.configure(state="normal")
|
||||||
# to signal the thread to stop.
|
self.cancel_button.configure(state="disabled")
|
||||||
messagebox.showinfo("Cancel", "Cancellation requested. The current operation will continue until it is finished.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
root = customtkinter.CTk()
|
root = customtkinter.CTk()
|
||||||
gui = VideoConverterGUI(root)
|
gui = VideoConverterGUI(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
BIN
images/video_converter.png
Normal file
BIN
images/video_converter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
56
test.css
Normal file
56
test.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
:root {
|
||||||
|
/** Base colors */
|
||||||
|
--clr-dark-a0: #000000;
|
||||||
|
--clr-light-a0: #ffffff;
|
||||||
|
|
||||||
|
/** Theme primary colors */
|
||||||
|
--clr-primary-a0: #759542;
|
||||||
|
--clr-primary-a10: #84a057;
|
||||||
|
--clr-primary-a20: #94ac6b;
|
||||||
|
--clr-primary-a30: #a3b77f;
|
||||||
|
--clr-primary-a40: #b2c394;
|
||||||
|
--clr-primary-a50: #c1cfa9;
|
||||||
|
|
||||||
|
/** Theme surface colors */
|
||||||
|
--clr-surface-a0: #121212;
|
||||||
|
--clr-surface-a10: #282828;
|
||||||
|
--clr-surface-a20: #3f3f3f;
|
||||||
|
--clr-surface-a30: #575757;
|
||||||
|
--clr-surface-a40: #717171;
|
||||||
|
--clr-surface-a50: #8b8b8b;
|
||||||
|
|
||||||
|
/** Theme tonal surface colors */
|
||||||
|
--clr-surface-tonal-a0: #1c1e17;
|
||||||
|
--clr-surface-tonal-a10: #31332c;
|
||||||
|
--clr-surface-tonal-a20: #474943;
|
||||||
|
--clr-surface-tonal-a30: #5f605b;
|
||||||
|
--clr-surface-tonal-a40: #777974;
|
||||||
|
--clr-surface-tonal-a50: #91928e;
|
||||||
|
|
||||||
|
/** Success colors */
|
||||||
|
--clr-success-a0: #22946e;
|
||||||
|
--clr-success-a10: #47d5a6;
|
||||||
|
--clr-success-a20: #9ae8ce;
|
||||||
|
|
||||||
|
/** Warning colors */
|
||||||
|
--clr-warning-a0: #a87a2a;
|
||||||
|
--clr-warning-a10: #d7ac61;
|
||||||
|
--clr-warning-a20: #ecd7b2;
|
||||||
|
|
||||||
|
/** Danger colors */
|
||||||
|
--clr-danger-a0: #9c2121;
|
||||||
|
--clr-danger-a10: #d94a4a;
|
||||||
|
--clr-danger-a20: #eb9e9e;
|
||||||
|
|
||||||
|
/** Info colors */
|
||||||
|
--clr-info-a0: #21498a;
|
||||||
|
--clr-info-a10: #4077d1;
|
||||||
|
--clr-info-a20: #92b2e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Examples */
|
||||||
|
.bg-primary {
|
||||||
|
color: var(--clr-primary-a50);
|
||||||
|
background-color: var(--clr-surface-a0);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ block_cipher = None
|
|||||||
a = Analysis(['main.py'],
|
a = Analysis(['main.py'],
|
||||||
pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'],
|
pathex=['/home/joe/Cloud9/Documents/Obisdian/projects/Video Converter'],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[('gui.py', '.')],
|
datas=[('gui.py', '.'), ('custom_theme_dark.json', '.'), ('custom_theme_light.json', '.')],
|
||||||
hiddenimports=[],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
Reference in New Issue
Block a user