Let's just create an installer for Windows to dumb it down

This commit is contained in:
2025-08-10 13:09:36 -05:00
parent 4eb71e74fc
commit 1eb761ec43
6 changed files with 1148 additions and 0 deletions

21
windows/LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 YouTube Channel Archiver
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
windows/README.txt Normal file
View File

@@ -0,0 +1,48 @@
YouTube Channel Archiver v1.0
==============================
A simple, user-friendly tool to download entire YouTube channels.
FEATURES:
✓ Download all videos from any YouTube channel
✓ Choose video quality (720p, 1080p, best quality, etc.)
✓ Audio-only downloads supported
✓ Download thumbnails and metadata
✓ Resume interrupted downloads automatically
✓ Automatic dependency management
✓ Clean, intuitive interface
SYSTEM REQUIREMENTS:
- Windows 10 or later (64-bit)
- Internet connection for downloads
- Sufficient disk space for videos
QUICK START:
1. Launch YouTube Channel Archiver
2. Paste any YouTube channel URL
3. Choose your download preferences
4. Select where to save the videos
5. Click "Download Channel"
SUPPORTED URL FORMATS:
- https://www.youtube.com/@channelname
- https://www.youtube.com/c/channelname
- https://www.youtube.com/user/username
- https://www.youtube.com/channel/UCxxxxxxxxx
The application will automatically:
- Install required dependencies (yt-dlp)
- Create organized folders by channel name
- Track downloaded videos to avoid duplicates
- Handle errors gracefully and continue downloading
TROUBLESHOOTING:
- Ensure you have a stable internet connection
- Check that the YouTube URL is correct and accessible
- Make sure you have enough disk space
- Some videos may be unavailable due to region restrictions
For support and updates, visit: https://github.com/yt-dlp/yt-dlp
LICENSE:
This software is provided as-is under the MIT License.

57
windows/build_windows.bat Normal file
View File

@@ -0,0 +1,57 @@
@echo off
title YouTube Channel Archiver - Windows Build Script
echo ================================================================
echo YouTube Channel Archiver - Windows Build Script
echo ================================================================
echo.
echo [1/5] Checking Python installation...
python --version > nul 2>&1
if errorlevel 1 (
echo ERROR: Python is not installed or not in PATH
echo Please install Python from https://python.org
pause
exit /b 1
)
python --version
echo.
echo [2/5] Installing/updating build dependencies...
python -m pip install --upgrade pip
python -m pip install pyinstaller
echo.
echo [3/5] Creating executable with PyInstaller...
pyinstaller --onefile --windowed --name "YouTubeChannelArchiver" --distpath dist --workpath build youtube_archiver_standalone.py
echo.
echo [4/5] Testing the executable...
if exist "dist\YouTubeChannelArchiver.exe" (
echo SUCCESS: Executable created at dist\YouTubeChannelArchiver.exe
) else (
echo ERROR: Executable was not created
pause
exit /b 1
)
echo.
echo [5/5] Creating portable package...
if not exist "portable" mkdir portable
copy "dist\YouTubeChannelArchiver.exe" "portable\"
echo Portable version created in 'portable' folder
echo.
echo ================================================================
echo BUILD COMPLETED SUCCESSFULLY!
echo ================================================================
echo.
echo Files created:
echo - dist\YouTubeChannelArchiver.exe (main executable)
echo - portable\YouTubeChannelArchiver.exe (portable version)
echo.
echo You can now:
echo 1. Test the executable by running it
echo 2. Distribute the portable folder
echo 3. Create an installer using the Inno Setup script below
echo.
pause

38
windows/installer.iss Normal file
View File

@@ -0,0 +1,38 @@
[Setup]
AppName=YouTube Channel Archiver
AppVersion=1.0.0
AppPublisher=YouTube Channel Archiver Team
AppPublisherURL=https://github.com/yt-dlp/yt-dlp
AppSupportURL=https://github.com/yt-dlp/yt-dlp
DefaultDirName={autopf}\YouTubeChannelArchiver
DefaultGroupName=YouTube Channel Archiver
AllowNoIcons=yes
LicenseFile=LICENSE.txt
OutputDir=installer_output
OutputBaseFilename=YouTubeChannelArchiver_v1.0_Setup
SetupIconFile=icon.ico
Compression=lzma
SolidCompression=yes
PrivilegesRequired=lowest
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "dist\YouTubeChannelArchiver.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "README.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\YouTube Channel Archiver"; Filename: "{app}\YouTubeChannelArchiver.exe"
Name: "{group}\{cm:ProgramOnTheWeb,YouTube Channel Archiver}"; Filename: "https://github.com/yt-dlp/yt-dlp"
Name: "{group}\{cm:UninstallProgram,YouTube Channel Archiver}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\YouTube Channel Archiver"; Filename: "{app}\YouTubeChannelArchiver.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\YouTubeChannelArchiver.exe"; Description: "{cm:LaunchProgram,YouTube Channel Archiver}"; Flags: nowait postinstall skipifsilent

View File

@@ -0,0 +1,492 @@
#!/usr/bin/env python3
"""
YouTube Channel Archiver - Standalone Windows Executable
Complete self-contained application with dependency management.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import subprocess
import threading
import sys
import os
import platform
from pathlib import Path
import queue
import tempfile
class YouTubeArchiverStandalone:
def __init__(self, root):
self.root = root
self.root.title("YouTube Channel Archiver v1.0")
self.root.geometry("700x600")
self.root.resizable(True, True)
# Queue for thread communication
self.output_queue = queue.Queue()
# Variables
self.channel_url = tk.StringVar()
self.output_dir = tk.StringVar(value=str(Path.home() / "Downloads" / "YouTube_Archives"))
self.quality = tk.StringVar(value="best")
self.audio_only = tk.BooleanVar()
self.download_thumbnails = tk.BooleanVar(value=True)
self.download_metadata = tk.BooleanVar(value=True)
# Track processes
self.download_process = None
self.is_downloading = False
self.dependencies_checked = False
self.create_widgets()
# Check dependencies in background
self.root.after(1000, self.check_all_dependencies)
# Start checking queue for output updates
self.root.after(100, self.check_queue)
def create_widgets(self):
# Main frame with padding
main_frame = ttk.Frame(self.root, padding="15")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Title and info
title_frame = ttk.Frame(main_frame)
title_frame.grid(row=0, column=0, columnspan=3, pady=(0, 20))
title_label = ttk.Label(title_frame, text="YouTube Channel Archiver",
font=('Arial', 18, 'bold'))
title_label.pack()
subtitle_label = ttk.Label(title_frame, text="Download entire YouTube channels with ease",
font=('Arial', 10))
subtitle_label.pack()
# Status frame
status_frame = ttk.LabelFrame(main_frame, text="System Status", padding="10")
status_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
status_frame.columnconfigure(1, weight=1)
self.python_status = ttk.Label(status_frame, text="Python: Checking...", foreground="orange")
self.python_status.grid(row=0, column=0, sticky=tk.W, pady=2)
self.ytdlp_status = ttk.Label(status_frame, text="yt-dlp: Checking...", foreground="orange")
self.ytdlp_status.grid(row=1, column=0, sticky=tk.W, pady=2)
# Channel URL input
ttk.Label(main_frame, text="YouTube Channel URL:", font=('Arial', 10, 'bold')).grid(
row=2, column=0, sticky=tk.W, pady=(5, 2))
url_frame = ttk.Frame(main_frame)
url_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
url_frame.columnconfigure(0, weight=1)
url_entry = ttk.Entry(url_frame, textvariable=self.channel_url, font=('Arial', 10))
url_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
paste_btn = ttk.Button(url_frame, text="Paste", command=self.paste_from_clipboard)
paste_btn.grid(row=0, column=1)
# Examples
examples_label = ttk.Label(main_frame,
text="Examples: https://www.youtube.com/@channelname or https://www.youtube.com/c/channelname",
font=('Arial', 8), foreground="gray")
examples_label.grid(row=4, column=0, columnspan=3, sticky=tk.W)
# Output directory
ttk.Label(main_frame, text="Download Location:", font=('Arial', 10, 'bold')).grid(
row=5, column=0, sticky=tk.W, pady=(15, 2))
dir_frame = ttk.Frame(main_frame)
dir_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
dir_frame.columnconfigure(0, weight=1)
dir_entry = ttk.Entry(dir_frame, textvariable=self.output_dir, font=('Arial', 10))
dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_directory)
browse_btn.grid(row=0, column=1)
# Options frame
options_frame = ttk.LabelFrame(main_frame, text="Download Options", padding="10")
options_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
options_frame.columnconfigure(1, weight=1)
# Quality selection
ttk.Label(options_frame, text="Video Quality:").grid(row=0, column=0, sticky=tk.W, pady=2)
quality_combo = ttk.Combobox(options_frame, textvariable=self.quality,
values=["best", "1080p", "720p", "480p", "worst"],
state="readonly", width=15)
quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0))
# Checkboxes
ttk.Checkbutton(options_frame, text="Audio only",
variable=self.audio_only).grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Checkbutton(options_frame, text="Download thumbnails",
variable=self.download_thumbnails).grid(row=1, column=1, sticky=tk.W, pady=2)
ttk.Checkbutton(options_frame, text="Download metadata",
variable=self.download_metadata).grid(row=2, column=0, sticky=tk.W, pady=2)
# Download button
self.download_btn = ttk.Button(main_frame, text="Download Channel",
command=self.start_download,
style="Accent.TButton")
self.download_btn.grid(row=8, column=0, columnspan=3, pady=15)
self.download_btn.state(['disabled']) # Disabled until dependencies are ready
# Progress bar
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
# Output text area
output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5")
output_frame.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(10, weight=1)
self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80,
font=('Consolas', 9))
self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Status bar
self.status_var = tk.StringVar(value="Checking system dependencies...")
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
relief=tk.SUNKEN, anchor=tk.W)
status_bar.grid(row=11, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
def paste_from_clipboard(self):
"""Paste URL from clipboard."""
try:
clipboard_content = self.root.clipboard_get()
if "youtube.com" in clipboard_content or "youtu.be" in clipboard_content:
self.channel_url.set(clipboard_content.strip())
else:
messagebox.showwarning("Invalid URL", "Clipboard doesn't contain a YouTube URL")
except tk.TkError:
messagebox.showwarning("Clipboard Error", "Could not access clipboard")
def log_output(self, message):
"""Add message to output text area."""
self.output_text.insert(tk.END, message + "\n")
self.output_text.see(tk.END)
self.root.update_idletasks()
def clear_output(self):
"""Clear the output text area."""
self.output_text.delete(1.0, tk.END)
def check_all_dependencies(self):
"""Check all system dependencies."""
self.log_output("=== System Dependency Check ===")
# Check Python
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
self.log_output(f"Python {python_version} detected")
self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green")
# Check yt-dlp in background thread
threading.Thread(target=self.check_ytdlp, daemon=True).start()
def check_ytdlp(self):
"""Check if yt-dlp is available, install if needed."""
try:
# Try to run yt-dlp
result = subprocess.run(['yt-dlp', '--version'],
capture_output=True, text=True, check=True, timeout=10)
version = result.stdout.strip()
# Update UI from main thread
self.root.after(0, lambda: self.ytdlp_found(version))
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
# yt-dlp not found, try to install
self.root.after(0, lambda: self.install_ytdlp())
def ytdlp_found(self, version):
"""Called when yt-dlp is found."""
self.log_output(f"yt-dlp {version} is available")
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
self.dependencies_ready()
def install_ytdlp(self):
"""Install yt-dlp using pip."""
self.log_output("yt-dlp not found. Installing...")
self.ytdlp_status.config(text="yt-dlp: Installing...", foreground="orange")
# Install in background thread
threading.Thread(target=self.do_install_ytdlp, daemon=True).start()
def do_install_ytdlp(self):
"""Actually install yt-dlp."""
try:
# Install yt-dlp
result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'yt-dlp'],
capture_output=True, text=True, check=True, timeout=60)
# Verify installation
result = subprocess.run(['yt-dlp', '--version'],
capture_output=True, text=True, check=True, timeout=10)
version = result.stdout.strip()
# Update UI from main thread
self.root.after(0, lambda: self.ytdlp_installed(version))
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
self.root.after(0, lambda: self.ytdlp_install_failed(str(e)))
def ytdlp_installed(self, version):
"""Called when yt-dlp is successfully installed."""
self.log_output(f"yt-dlp {version} installed successfully!")
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
self.dependencies_ready()
def ytdlp_install_failed(self, error):
"""Called when yt-dlp installation fails."""
self.log_output(f"Failed to install yt-dlp: {error}")
self.ytdlp_status.config(text="yt-dlp: Installation failed [ERROR]", foreground="red")
self.status_var.set("Error: Could not install yt-dlp. Check internet connection.")
messagebox.showerror("Installation Error",
"Could not install yt-dlp automatically.\n\n"
"Please check your internet connection and try again.\n"
"You can also install it manually with: pip install yt-dlp")
def dependencies_ready(self):
"""Called when all dependencies are ready."""
if not self.dependencies_checked:
self.dependencies_checked = True
self.download_btn.state(['!disabled'])
self.status_var.set("Ready to download!")
self.log_output("=== All dependencies ready! ===")
def browse_directory(self):
"""Open directory browser."""
directory = filedialog.askdirectory(initialdir=self.output_dir.get())
if directory:
self.output_dir.set(directory)
def validate_inputs(self):
"""Validate user inputs."""
if not self.channel_url.get().strip():
messagebox.showerror("Error", "Please enter a YouTube channel URL")
return False
url = self.channel_url.get().strip()
if "youtube.com" not in url and "youtu.be" not in url:
messagebox.showerror("Error", "Please enter a valid YouTube URL")
return False
if not self.output_dir.get().strip():
messagebox.showerror("Error", "Please select an output directory")
return False
# Check if output directory exists or can be created
try:
output_path = Path(self.output_dir.get())
output_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
messagebox.showerror("Error", f"Cannot create output directory: {e}")
return False
return True
def build_command(self):
"""Build the yt-dlp command based on GUI settings."""
cmd = ['yt-dlp']
# Output template
output_path = Path(self.output_dir.get())
output_template = str(output_path / "%(uploader)s" / "%(upload_date)s - %(title)s.%(ext)s")
cmd.extend(['-o', output_template])
# Quality settings
if self.audio_only.get():
cmd.extend(['-f', 'bestaudio/best'])
else:
if self.quality.get() == 'best':
cmd.extend(['-f', 'best'])
elif self.quality.get() == 'worst':
cmd.extend(['-f', 'worst'])
else:
# Custom quality
quality_num = self.quality.get().replace('p', '')
cmd.extend(['-f', f'best[height<={quality_num}]'])
# Additional options
cmd.extend([
'--ignore-errors',
'--no-overwrites',
'--continue',
])
# Archive file
archive_file = output_path / 'download_archive.txt'
cmd.extend(['--download-archive', str(archive_file)])
# Metadata and thumbnails
if self.download_metadata.get():
cmd.extend(['--write-info-json', '--write-description'])
if self.download_thumbnails.get():
cmd.extend(['--write-thumbnail'])
# Add channel URL
cmd.append(self.channel_url.get().strip())
return cmd
def start_download(self):
"""Start the download process."""
if self.is_downloading:
self.stop_download()
return
if not self.validate_inputs():
return
# Clear previous output
self.clear_output()
# Update UI
self.is_downloading = True
self.download_btn.config(text="Stop Download")
self.progress.start()
self.status_var.set("Downloading...")
# Build command
cmd = self.build_command()
self.log_output(f"Starting download...")
self.log_output(f"Channel: {self.channel_url.get()}")
self.log_output(f"Output: {self.output_dir.get()}")
self.log_output("-" * 50)
# Start download in separate thread
self.download_thread = threading.Thread(target=self.run_download, args=(cmd,))
self.download_thread.daemon = True
self.download_thread.start()
def run_download(self, cmd):
"""Run the download process."""
try:
# Start the process
self.download_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
)
# Read output line by line
for line in iter(self.download_process.stdout.readline, ''):
if not self.is_downloading:
break
self.output_queue.put(('output', line.rstrip()))
# Wait for process to complete
return_code = self.download_process.wait()
if return_code == 0:
self.output_queue.put(('status', 'Download completed successfully!'))
else:
self.output_queue.put(('status', f'Download finished with some errors (code {return_code})'))
except Exception as e:
self.output_queue.put(('error', f'Error during download: {str(e)}'))
finally:
self.output_queue.put(('finished', None))
def stop_download(self):
"""Stop the current download."""
if self.download_process and self.download_process.poll() is None:
self.download_process.terminate()
self.log_output("\n[WARNING] Download stopped by user")
self.download_finished()
def download_finished(self):
"""Handle download completion."""
self.is_downloading = False
self.download_btn.config(text="Download Channel")
self.progress.stop()
self.status_var.set("Ready to download!")
def check_queue(self):
"""Check the queue for messages from the download thread."""
try:
while True:
msg_type, message = self.output_queue.get_nowait()
if msg_type == 'output':
self.log_output(message)
elif msg_type == 'status':
self.log_output(f"\n{message}")
self.status_var.set(message)
elif msg_type == 'error':
self.log_output(f"\n[ERROR] {message}")
messagebox.showerror("Download Error", message)
elif msg_type == 'finished':
self.download_finished()
break
except queue.Empty:
pass
# Schedule next check
self.root.after(100, self.check_queue)
def main():
# Set up the main window
root = tk.Tk()
# Configure styling
style = ttk.Style()
try:
style.theme_use('vista') # Windows native theme
except:
try:
style.theme_use('clam') # Fallback modern theme
except:
pass # Use default
# Configure button style
style.configure('Accent.TButton', font=('Arial', 10, 'bold'))
# Create the application
app = YouTubeArchiverStandalone(root)
# Handle window closing
def on_closing():
if app.is_downloading:
if messagebox.askokcancel("Quit", "Download in progress. Stop and quit?"):
app.stop_download()
root.destroy()
else:
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# Center window on screen
root.update_idletasks()
x = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
y = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
root.geometry(f"+{x}+{y}")
# Set minimum size
root.minsize(600, 500)
# Run the application
root.mainloop()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,492 @@
#!/usr/bin/env python3
"""
YouTube Channel Archiver - Standalone Windows Executable
Complete self-contained application with dependency management.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import subprocess
import threading
import sys
import os
import platform
from pathlib import Path
import queue
import tempfile
class YouTubeArchiverStandalone:
def __init__(self, root):
self.root = root
self.root.title("YouTube Channel Archiver v1.0")
self.root.geometry("700x600")
self.root.resizable(True, True)
# Queue for thread communication
self.output_queue = queue.Queue()
# Variables
self.channel_url = tk.StringVar()
self.output_dir = tk.StringVar(value=str(Path.home() / "Downloads" / "YouTube_Archives"))
self.quality = tk.StringVar(value="best")
self.audio_only = tk.BooleanVar()
self.download_thumbnails = tk.BooleanVar(value=True)
self.download_metadata = tk.BooleanVar(value=True)
# Track processes
self.download_process = None
self.is_downloading = False
self.dependencies_checked = False
self.create_widgets()
# Check dependencies in background
self.root.after(1000, self.check_all_dependencies)
# Start checking queue for output updates
self.root.after(100, self.check_queue)
def create_widgets(self):
# Main frame with padding
main_frame = ttk.Frame(self.root, padding="15")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Title and info
title_frame = ttk.Frame(main_frame)
title_frame.grid(row=0, column=0, columnspan=3, pady=(0, 20))
title_label = ttk.Label(title_frame, text="YouTube Channel Archiver",
font=('Arial', 18, 'bold'))
title_label.pack()
subtitle_label = ttk.Label(title_frame, text="Download entire YouTube channels with ease",
font=('Arial', 10))
subtitle_label.pack()
# Status frame
status_frame = ttk.LabelFrame(main_frame, text="System Status", padding="10")
status_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
status_frame.columnconfigure(1, weight=1)
self.python_status = ttk.Label(status_frame, text="Python: Checking...", foreground="orange")
self.python_status.grid(row=0, column=0, sticky=tk.W, pady=2)
self.ytdlp_status = ttk.Label(status_frame, text="yt-dlp: Checking...", foreground="orange")
self.ytdlp_status.grid(row=1, column=0, sticky=tk.W, pady=2)
# Channel URL input
ttk.Label(main_frame, text="YouTube Channel URL:", font=('Arial', 10, 'bold')).grid(
row=2, column=0, sticky=tk.W, pady=(5, 2))
url_frame = ttk.Frame(main_frame)
url_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
url_frame.columnconfigure(0, weight=1)
url_entry = ttk.Entry(url_frame, textvariable=self.channel_url, font=('Arial', 10))
url_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
paste_btn = ttk.Button(url_frame, text="Paste", command=self.paste_from_clipboard)
paste_btn.grid(row=0, column=1)
# Examples
examples_label = ttk.Label(main_frame,
text="Examples: https://www.youtube.com/@channelname or https://www.youtube.com/c/channelname",
font=('Arial', 8), foreground="gray")
examples_label.grid(row=4, column=0, columnspan=3, sticky=tk.W)
# Output directory
ttk.Label(main_frame, text="Download Location:", font=('Arial', 10, 'bold')).grid(
row=5, column=0, sticky=tk.W, pady=(15, 2))
dir_frame = ttk.Frame(main_frame)
dir_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
dir_frame.columnconfigure(0, weight=1)
dir_entry = ttk.Entry(dir_frame, textvariable=self.output_dir, font=('Arial', 10))
dir_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_directory)
browse_btn.grid(row=0, column=1)
# Options frame
options_frame = ttk.LabelFrame(main_frame, text="Download Options", padding="10")
options_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 15))
options_frame.columnconfigure(1, weight=1)
# Quality selection
ttk.Label(options_frame, text="Video Quality:").grid(row=0, column=0, sticky=tk.W, pady=2)
quality_combo = ttk.Combobox(options_frame, textvariable=self.quality,
values=["best", "1080p", "720p", "480p", "worst"],
state="readonly", width=15)
quality_combo.grid(row=0, column=1, sticky=tk.W, pady=2, padx=(10, 0))
# Checkboxes
ttk.Checkbutton(options_frame, text="Audio only",
variable=self.audio_only).grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Checkbutton(options_frame, text="Download thumbnails",
variable=self.download_thumbnails).grid(row=1, column=1, sticky=tk.W, pady=2)
ttk.Checkbutton(options_frame, text="Download metadata",
variable=self.download_metadata).grid(row=2, column=0, sticky=tk.W, pady=2)
# Download button
self.download_btn = ttk.Button(main_frame, text="Download Channel",
command=self.start_download,
style="Accent.TButton")
self.download_btn.grid(row=8, column=0, columnspan=3, pady=15)
self.download_btn.state(['disabled']) # Disabled until dependencies are ready
# Progress bar
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=9, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
# Output text area
output_frame = ttk.LabelFrame(main_frame, text="Download Progress", padding="5")
output_frame.grid(row=10, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
output_frame.columnconfigure(0, weight=1)
output_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(10, weight=1)
self.output_text = scrolledtext.ScrolledText(output_frame, height=10, width=80,
font=('Consolas', 9))
self.output_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Status bar
self.status_var = tk.StringVar(value="Checking system dependencies...")
status_bar = ttk.Label(main_frame, textvariable=self.status_var,
relief=tk.SUNKEN, anchor=tk.W)
status_bar.grid(row=11, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5, 0))
def paste_from_clipboard(self):
"""Paste URL from clipboard."""
try:
clipboard_content = self.root.clipboard_get()
if "youtube.com" in clipboard_content or "youtu.be" in clipboard_content:
self.channel_url.set(clipboard_content.strip())
else:
messagebox.showwarning("Invalid URL", "Clipboard doesn't contain a YouTube URL")
except tk.TkError:
messagebox.showwarning("Clipboard Error", "Could not access clipboard")
def log_output(self, message):
"""Add message to output text area."""
self.output_text.insert(tk.END, message + "\n")
self.output_text.see(tk.END)
self.root.update_idletasks()
def clear_output(self):
"""Clear the output text area."""
self.output_text.delete(1.0, tk.END)
def check_all_dependencies(self):
"""Check all system dependencies."""
self.log_output("=== System Dependency Check ===")
# Check Python
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
self.log_output(f"Python {python_version} detected")
self.python_status.config(text=f"Python: {python_version} [OK]", foreground="green")
# Check yt-dlp in background thread
threading.Thread(target=self.check_ytdlp, daemon=True).start()
def check_ytdlp(self):
"""Check if yt-dlp is available, install if needed."""
try:
# Try to run yt-dlp
result = subprocess.run(['yt-dlp', '--version'],
capture_output=True, text=True, check=True, timeout=10)
version = result.stdout.strip()
# Update UI from main thread
self.root.after(0, lambda: self.ytdlp_found(version))
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
# yt-dlp not found, try to install
self.root.after(0, lambda: self.install_ytdlp())
def ytdlp_found(self, version):
"""Called when yt-dlp is found."""
self.log_output(f"yt-dlp {version} is available")
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
self.dependencies_ready()
def install_ytdlp(self):
"""Install yt-dlp using pip."""
self.log_output("yt-dlp not found. Installing...")
self.ytdlp_status.config(text="yt-dlp: Installing...", foreground="orange")
# Install in background thread
threading.Thread(target=self.do_install_ytdlp, daemon=True).start()
def do_install_ytdlp(self):
"""Actually install yt-dlp."""
try:
# Install yt-dlp
result = subprocess.run([sys.executable, '-m', 'pip', 'install', 'yt-dlp'],
capture_output=True, text=True, check=True, timeout=60)
# Verify installation
result = subprocess.run(['yt-dlp', '--version'],
capture_output=True, text=True, check=True, timeout=10)
version = result.stdout.strip()
# Update UI from main thread
self.root.after(0, lambda: self.ytdlp_installed(version))
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
self.root.after(0, lambda: self.ytdlp_install_failed(str(e)))
def ytdlp_installed(self, version):
"""Called when yt-dlp is successfully installed."""
self.log_output(f"yt-dlp {version} installed successfully!")
self.ytdlp_status.config(text=f"yt-dlp: {version} [OK]", foreground="green")
self.dependencies_ready()
def ytdlp_install_failed(self, error):
"""Called when yt-dlp installation fails."""
self.log_output(f"Failed to install yt-dlp: {error}")
self.ytdlp_status.config(text="yt-dlp: Installation failed [ERROR]", foreground="red")
self.status_var.set("Error: Could not install yt-dlp. Check internet connection.")
messagebox.showerror("Installation Error",
"Could not install yt-dlp automatically.\n\n"
"Please check your internet connection and try again.\n"
"You can also install it manually with: pip install yt-dlp")
def dependencies_ready(self):
"""Called when all dependencies are ready."""
if not self.dependencies_checked:
self.dependencies_checked = True
self.download_btn.state(['!disabled'])
self.status_var.set("Ready to download!")
self.log_output("=== All dependencies ready! ===")
def browse_directory(self):
"""Open directory browser."""
directory = filedialog.askdirectory(initialdir=self.output_dir.get())
if directory:
self.output_dir.set(directory)
def validate_inputs(self):
"""Validate user inputs."""
if not self.channel_url.get().strip():
messagebox.showerror("Error", "Please enter a YouTube channel URL")
return False
url = self.channel_url.get().strip()
if "youtube.com" not in url and "youtu.be" not in url:
messagebox.showerror("Error", "Please enter a valid YouTube URL")
return False
if not self.output_dir.get().strip():
messagebox.showerror("Error", "Please select an output directory")
return False
# Check if output directory exists or can be created
try:
output_path = Path(self.output_dir.get())
output_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
messagebox.showerror("Error", f"Cannot create output directory: {e}")
return False
return True
def build_command(self):
"""Build the yt-dlp command based on GUI settings."""
cmd = ['yt-dlp']
# Output template
output_path = Path(self.output_dir.get())
output_template = str(output_path / "%(uploader)s" / "%(upload_date)s - %(title)s.%(ext)s")
cmd.extend(['-o', output_template])
# Quality settings
if self.audio_only.get():
cmd.extend(['-f', 'bestaudio/best'])
else:
if self.quality.get() == 'best':
cmd.extend(['-f', 'best'])
elif self.quality.get() == 'worst':
cmd.extend(['-f', 'worst'])
else:
# Custom quality
quality_num = self.quality.get().replace('p', '')
cmd.extend(['-f', f'best[height<={quality_num}]'])
# Additional options
cmd.extend([
'--ignore-errors',
'--no-overwrites',
'--continue',
])
# Archive file
archive_file = output_path / 'download_archive.txt'
cmd.extend(['--download-archive', str(archive_file)])
# Metadata and thumbnails
if self.download_metadata.get():
cmd.extend(['--write-info-json', '--write-description'])
if self.download_thumbnails.get():
cmd.extend(['--write-thumbnail'])
# Add channel URL
cmd.append(self.channel_url.get().strip())
return cmd
def start_download(self):
"""Start the download process."""
if self.is_downloading:
self.stop_download()
return
if not self.validate_inputs():
return
# Clear previous output
self.clear_output()
# Update UI
self.is_downloading = True
self.download_btn.config(text="Stop Download")
self.progress.start()
self.status_var.set("Downloading...")
# Build command
cmd = self.build_command()
self.log_output(f"Starting download...")
self.log_output(f"Channel: {self.channel_url.get()}")
self.log_output(f"Output: {self.output_dir.get()}")
self.log_output("-" * 50)
# Start download in separate thread
self.download_thread = threading.Thread(target=self.run_download, args=(cmd,))
self.download_thread.daemon = True
self.download_thread.start()
def run_download(self, cmd):
"""Run the download process."""
try:
# Start the process
self.download_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
)
# Read output line by line
for line in iter(self.download_process.stdout.readline, ''):
if not self.is_downloading:
break
self.output_queue.put(('output', line.rstrip()))
# Wait for process to complete
return_code = self.download_process.wait()
if return_code == 0:
self.output_queue.put(('status', 'Download completed successfully!'))
else:
self.output_queue.put(('status', f'Download finished with some errors (code {return_code})'))
except Exception as e:
self.output_queue.put(('error', f'Error during download: {str(e)}'))
finally:
self.output_queue.put(('finished', None))
def stop_download(self):
"""Stop the current download."""
if self.download_process and self.download_process.poll() is None:
self.download_process.terminate()
self.log_output("\n[WARNING] Download stopped by user")
self.download_finished()
def download_finished(self):
"""Handle download completion."""
self.is_downloading = False
self.download_btn.config(text="Download Channel")
self.progress.stop()
self.status_var.set("Ready to download!")
def check_queue(self):
"""Check the queue for messages from the download thread."""
try:
while True:
msg_type, message = self.output_queue.get_nowait()
if msg_type == 'output':
self.log_output(message)
elif msg_type == 'status':
self.log_output(f"\n{message}")
self.status_var.set(message)
elif msg_type == 'error':
self.log_output(f"\n[ERROR] {message}")
messagebox.showerror("Download Error", message)
elif msg_type == 'finished':
self.download_finished()
break
except queue.Empty:
pass
# Schedule next check
self.root.after(100, self.check_queue)
def main():
# Set up the main window
root = tk.Tk()
# Configure styling
style = ttk.Style()
try:
style.theme_use('vista') # Windows native theme
except:
try:
style.theme_use('clam') # Fallback modern theme
except:
pass # Use default
# Configure button style
style.configure('Accent.TButton', font=('Arial', 10, 'bold'))
# Create the application
app = YouTubeArchiverStandalone(root)
# Handle window closing
def on_closing():
if app.is_downloading:
if messagebox.askokcancel("Quit", "Download in progress. Stop and quit?"):
app.stop_download()
root.destroy()
else:
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# Center window on screen
root.update_idletasks()
x = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
y = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
root.geometry(f"+{x}+{y}")
# Set minimum size
root.minsize(600, 500)
# Run the application
root.mainloop()
if __name__ == "__main__":
main()