Initial version: basic Tkinter video concatenator

- Plain tkinter GUI: file picker, listbox of selected videos
- Move Up / Move Down buttons to reorder the sequence
- Output name entry + output directory chooser
- Optional 'delete originals after concatenation' checkbox
- Concatenates with the ffmpeg concat demuxer (-c copy) on a worker thread
- Crude fixed time estimate (5s-per-file countdown before running ffmpeg)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 13:26:59 +00:00
commit c26a8fc235
2 changed files with 178 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
video_concat_v2.1.py
video_concat_v3.py
video_concat_v4.py
video_concat_v4.1.py

174
video_concat.py Executable file
View File

@@ -0,0 +1,174 @@
import tkinter as tk
from tkinter import filedialog, messagebox, Listbox
import os
import subprocess
import threading
import time
class VideoConcatenatorApp:
def __init__(self, root):
self.root = root
self.root.title("Video Concatenator")
# Selected files
self.selected_files = []
self.output_file_name = "output.mp4"
self.delete_original = tk.BooleanVar(value=False)
# Section 1: Select files
self.label1 = tk.Label(root, text="1. Select Video Files", font=("Arial", 12))
self.label1.pack(pady=5)
self.select_btn = tk.Button(root, text="Select Videos", command=self.select_videos)
self.select_btn.pack(pady=5)
self.files_listbox = Listbox(root, selectmode=tk.SINGLE, width=80, height=10)
self.files_listbox.pack(pady=5)
# Section 2: Organize sequence
self.label2 = tk.Label(root, text="2. Organize Sequence", font=("Arial", 12))
self.label2.pack(pady=5)
self.up_btn = tk.Button(root, text="Move Up", command=self.move_up)
self.up_btn.pack(pady=5)
self.down_btn = tk.Button(root, text="Move Down", command=self.move_down)
self.down_btn.pack(pady=5)
# Section 3: Name the output file
self.label3 = tk.Label(root, text="3. Name the Output File", font=("Arial", 12))
self.label3.pack(pady=5)
self.output_name_entry = tk.Entry(root, width=50)
self.output_name_entry.insert(0, "output.mp4")
self.output_name_entry.pack(pady=5)
# Section 4: Choose output directory
self.label4 = tk.Label(root, text="4. Select Output Directory", font=("Arial", 12))
self.label4.pack(pady=5)
self.output_dir_btn = tk.Button(root, text="Choose Output Directory", command=self.select_output_dir)
self.output_dir_btn.pack(pady=5)
self.output_dir_label = tk.Label(root, text="", fg="blue")
self.output_dir_label.pack(pady=5)
# Section 5: Delete original files option
self.delete_original_check = tk.Checkbutton(root, text="Delete Original Files After Concatenation", variable=self.delete_original)
self.delete_original_check.pack(pady=5)
# Concatenate Button
self.concat_btn = tk.Button(root, text="Concatenate Videos", command=self.start_concatenation, state=tk.DISABLED)
self.concat_btn.pack(pady=10)
# Time Left Label
self.time_left_label = tk.Label(root, text="", font=("Arial", 10), fg="green")
self.time_left_label.pack(pady=5)
def select_videos(self):
files = filedialog.askopenfilenames(filetypes=[("Video Files", "*.mp4;*.mov;*.mkv;*.avi;*.flv")])
if files:
self.selected_files = list(files)
self.update_files_listbox()
self.concat_btn.config(state=tk.NORMAL)
def update_files_listbox(self):
self.files_listbox.delete(0, tk.END)
for file in self.selected_files:
self.files_listbox.insert(tk.END, file)
def move_up(self):
selected = self.files_listbox.curselection()
if selected and selected[0] > 0:
index = selected[0]
self.selected_files[index], self.selected_files[index - 1] = self.selected_files[index - 1], self.selected_files[index]
self.update_files_listbox()
self.files_listbox.select_set(index - 1)
def move_down(self):
selected = self.files_listbox.curselection()
if selected and selected[0] < len(self.selected_files) - 1:
index = selected[0]
self.selected_files[index], self.selected_files[index + 1] = self.selected_files[index + 1], self.selected_files[index]
self.update_files_listbox()
self.files_listbox.select_set(index + 1)
def select_output_dir(self):
directory = filedialog.askdirectory()
if directory:
self.output_dir = directory
self.output_dir_label.config(text=directory)
def start_concatenation(self):
# Disable button to prevent re-clicking
self.concat_btn.config(state=tk.DISABLED)
self.time_left_label.config(text="Starting concatenation...")
# Run the concatenation process in a separate thread
threading.Thread(target=self.concatenate_videos).start()
def concatenate_videos(self):
if not self.selected_files:
messagebox.showerror("Error", "No videos selected.")
return
if not hasattr(self, "output_dir"):
messagebox.showerror("Error", "Output directory not selected.")
return
# Get output file name
self.output_file_name = self.output_name_entry.get().strip()
if not self.output_file_name:
messagebox.showerror("Error", "Output file name cannot be empty.")
return
# Ensure the output file name ends with .mp4
if not self.output_file_name.endswith(".mp4"):
self.output_file_name += ".mp4"
# Create file list for FFmpeg
temp_file_list = os.path.join(self.output_dir, "file_list.txt")
with open(temp_file_list, "w") as f:
for file in self.selected_files:
f.write(f"file '{file}'\n")
# Output file path
output_file = os.path.join(self.output_dir, self.output_file_name)
# FFmpeg command
command = [
"ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", temp_file_list,
"-c", "copy",
output_file
]
try:
total_time = len(self.selected_files) * 5 # Estimate: ~5 seconds per video
for i in range(total_time):
time.sleep(1)
self.time_left_label.config(text=f"Estimated time left: {total_time - i}s")
self.root.update_idletasks()
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.time_left_label.config(text="Concatenation completed!")
messagebox.showinfo("Success", f"Videos concatenated successfully! Output: {output_file}")
# Optionally delete original files
if self.delete_original.get():
for file in self.selected_files:
os.remove(file)
messagebox.showinfo("Info", "Original files have been deleted.")
except subprocess.CalledProcessError:
self.time_left_label.config(text="Error during concatenation.")
messagebox.showerror("Error", "Failed to concatenate videos.")
finally:
os.remove(temp_file_list)
self.concat_btn.config(state=tk.NORMAL)
if __name__ == "__main__":
root = tk.Tk()
app = VideoConcatenatorApp(root)
root.mainloop()