From 1b9437fea993330648b0a0b87c5d51a97ec341dc Mon Sep 17 00:00:00 2001 From: William P Date: Wed, 21 Jan 2026 14:15:59 -0500 Subject: [PATCH] blank slate go app --- app/go.mod | 3 + app/main.go | 7 ++ app/main.py | 216 ------------------------------------------- app/misc.py | 23 ----- app/requirements.txt | 3 - app/ytdlp.py | 65 ------------- 6 files changed, 10 insertions(+), 307 deletions(-) create mode 100644 app/go.mod create mode 100644 app/main.go delete mode 100644 app/main.py delete mode 100644 app/misc.py delete mode 100644 app/requirements.txt delete mode 100644 app/ytdlp.py diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..2fe3137 --- /dev/null +++ b/app/go.mod @@ -0,0 +1,3 @@ +module git.dubyatp.xyz/williamp/yt-dlp-bot + +go 1.25.2 diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..e373dd8 --- /dev/null +++ b/app/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello bitches") +} \ No newline at end of file diff --git a/app/main.py b/app/main.py deleted file mode 100644 index ba05390..0000000 --- a/app/main.py +++ /dev/null @@ -1,216 +0,0 @@ -import asyncio -import os -from dotenv import load_dotenv -from discord import Intents, Client, Interaction, SelectOption, Message, app_commands, ui - -from misc import extract_url_from_string, render_progress_bar -import ytdlp - -load_dotenv() - -discord_token = os.getenv("DISCORD_TOKEN") -out_path = os.getenv("OUT_PATH") -temp_path = os.getenv("TEMP_PATH") - -class DownloadVideo(ui.View): - def __init__(self, url: str): - super().__init__() - self.url = url - self.video_options = [] - self.audio_options = [] - self.selected_video = None - self.selected_audio = None - self._populate_format_options() - - self.video_select = ui.Select( - placeholder="Choose a video format...", - options=self.video_options, - min_values=1, - max_values=1, - custom_id="video_select" - ) - self.video_select.callback = self.on_video_select - self.add_item(self.video_select) - - self.audio_select = ui.Select( - placeholder="Choose an audio format...", - options=self.audio_options, - min_values=1, - max_values=1, - custom_id="audio_select", - disabled=True # Start disabled - ) - self.audio_select.callback = self.on_audio_select - self.add_item(self.audio_select) - - def _populate_format_options(self): - try: - formats = ytdlp.get_formats(self.url) - - # Sort video options by height and bitrate - sorted_videos = sorted( - formats['video_options'], - key=lambda v: (v.get('height', 0), v.get('tbr', 0)), - reverse=True - ) - - self.video_options = [ - SelectOption( - label=f"{v['resolution']} {v['height']}p ({v['ext']}, {int(v['tbr'] or 0)}kbps)", - value=v['format_id'] - ) - for v in sorted_videos[:25] # limit - ] - self.audio_options = [ - SelectOption( - label=f"{a['format']} ({a['ext']}, {int(a['tbr'] or 0)}kbps" - + (f", {a['language']}" if a.get('language') else "") + ")", - value=a['format_id'] - ) - for a in formats['audio_options'][:25] # limit - ] - # Ensure at least one option for each select - if not self.video_options: - self.video_options = [SelectOption(label="No video formats found", value="none", default=True, description="No video formats available", disabled=True)] - if not self.audio_options: - self.audio_options = [SelectOption(label="No audio formats found", value="none", default=True, description="No audio formats available", disabled=True)] - except Exception as e: - print(f"Error fetching formats: {e}") - self.video_options = [SelectOption(label="Error loading formats", value="none", default=True, description="Error loading formats", disabled=True)] - self.audio_options = [SelectOption(label="Error loading formats", value="none", default=True, description="Error loading formats", disabled=True)] - - async def on_video_select(self, interaction: Interaction): - self.selected_video = self.video_select.values[0] - # Disable video select, enable audio select - self.video_select.disabled = True - self.audio_select.disabled = False - await interaction.response.edit_message( - content="Now select an audio format:", - view=self - ) - - async def on_audio_select(self, interaction: Interaction): - self.selected_audio = self.audio_select.values[0] - format_string = f"{self.selected_video}+{self.selected_audio}" - # Optionally, disable both selects after selection - self.audio_select.disabled = True - - # Get labels for summary - video_label = next((o.label for o in self.video_options if o.value == self.selected_video), self.selected_video) - audio_label = next((o.label for o in self.audio_options if o.value == self.selected_audio), self.selected_audio) - format_string = f"{self.selected_video}+{self.selected_audio}" - - # Show progress bar - await interaction.response.edit_message( - content=f"Downloading video: {render_progress_bar(0)}", - view=self - ) - - progress_queue = asyncio.Queue() - stop_event = asyncio.Event() - - async def progress_worker(msg): - last_percent = 0 - while not stop_event.is_set(): - try: - percent = await asyncio.wait_for(progress_queue.get(), timeout=2) - # Only update every 10% or when at 100% - if percent // 10 > last_percent // 10 or percent == 100: - await msg.edit(content=f"Downloading video: {render_progress_bar(percent)}") - last_percent = percent - except asyncio.TimeoutError: - continue - - # Get message to edit - msg = await interaction.original_response() - - worker_task = asyncio.create_task(progress_worker(msg)) - - error = None - - async def progress_callback(percent): - await progress_queue.put(percent) - - loop = asyncio.get_running_loop() - def threaded_download(): - nonlocal error - try: - ytdlp.download_video( - self.url, format_string, out_path, temp_path, progress_callback, loop - ) - except Exception as e: - error = e - await asyncio.to_thread(threaded_download) - - stop_event.set() - await worker_task - - if error: - await msg.edit(content=f"❌ Download failed: {error}") - return - - # Show completion and summary - await msg.edit(content="✅ Download complete!\n" - ) - - # Send public message - await interaction.followup.send( - f"## Video Downloaded\n" - f"**URL**: {self.url}\n" - f"**Quality**: {video_label} + {audio_label}\n" - f"**Requested By**: <@{interaction.user.id}>" - ) - - -# Bot setup -intents = Intents.default() -intents.message_content = True -client = Client(intents=intents) -tree = app_commands.CommandTree(client) - -@client.event -async def on_ready() -> None: - """Called when the bot is ready.""" - print(f"Logged in as {client.user}") - await tree.sync() - -# /download command -@app_commands.allowed_contexts(dms=True, private_channels=True) -@app_commands.allowed_installs(users=True) -@tree.command(name="download", description="Download video and save it to 'youtube-vids'") -async def download_command(interaction: Interaction, url: str) -> None: - """Handles the /download command.""" - await interaction.response.defer(ephemeral=True, thinking=True) - - view = DownloadVideo(url) - await interaction.followup.send( - content="Select a video format:", - view=view, - ephemeral=True - ) - -# direct URL download from message context -@app_commands.allowed_contexts(dms=True, private_channels=True) -@app_commands.allowed_installs(users=True) -@tree.context_menu(name="download video") -async def download_context(interaction: Interaction, message: Message) -> None: - """Handles requests to download a URL from an already posted message""" - await interaction.response.defer(ephemeral=True, thinking=True) - - url = extract_url_from_string(message.clean_content) - if url is not None: - view = DownloadVideo(url) - await interaction.followup.send( - content="Select a video format:", - view=view, - ephemeral=True - ) - else: - await interaction.followup.send( - content="No URL was found in message, sorry :-(", - ephemeral=True - ) - - -if __name__ == "__main__": - client.run(discord_token) \ No newline at end of file diff --git a/app/misc.py b/app/misc.py deleted file mode 100644 index da7164f..0000000 --- a/app/misc.py +++ /dev/null @@ -1,23 +0,0 @@ -import re - -# Constants -PROGRESS_BAR_LENGTH = 10 - -def extract_url_from_string(text: str): - """ - Extracts the first URL from a string - """ - - url_pattern = re.compile(r'https?://\S+') - match = url_pattern.search(text) - - if match: - return match.group(0) - else: - return None - -def render_progress_bar(percent: int, length: int = PROGRESS_BAR_LENGTH) -> str: - """Renders a progress bar with the given percentage.""" - filled = int(length * percent // 100) - bar = "█" * filled + " " * (length - filled) - return f"[{bar}] {percent}%" \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt deleted file mode 100644 index d314c41..0000000 --- a/app/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -discord.py==2.6.4 -python-dotenv==1.2.1 -yt-dlp==2025.12.8 \ No newline at end of file diff --git a/app/ytdlp.py b/app/ytdlp.py deleted file mode 100644 index a111568..0000000 --- a/app/ytdlp.py +++ /dev/null @@ -1,65 +0,0 @@ -import yt_dlp -import asyncio - -def get_formats(url: str): - ydl = yt_dlp.YoutubeDL(params={ - 'remote_components': ['ejs:github'] - }) - info = ydl.extract_info(url, download=False) - - video_options = [] - audio_options = [] - - for fmt in info['formats']: - # Video-only - if fmt.get('vcodec') != 'none' and fmt.get('acodec') == 'none' and fmt.get('__needs_testing') == None: - video_options.append({ - 'height': fmt.get('height'), - 'resolution': fmt.get('resolution'), - 'format_id': fmt.get('format_id'), - 'ext': fmt.get('ext'), - 'tbr': fmt.get('tbr'), - }) - # Audio-only - elif fmt.get('acodec') != 'none' and fmt.get('vcodec') == 'none' and fmt.get('__needs_testing') == None: - audio_options.append({ - 'format': fmt.get('format'), - 'format_id': fmt.get('format_id'), - 'ext': fmt.get('ext'), - 'tbr': fmt.get('tbr'), - 'language': fmt.get('language') or fmt.get('language_preference'), - }) - - return { - 'video_options': video_options, - 'audio_options': audio_options, - } - -def download_video(url: str, format: str, out_path: str, temp_path: str, progress_callback, loop): - """ - Downloads a video and sends a progress callback as it downloads - Returns the format used - """ - progress = {'percent': 0} - - def hook(d): - if d['status'] == 'downloading': - total = d.get('total_bytes') or d.get('total_bytes_estimate') - downloaded = d.get('downloaded_bytes') - if total and downloaded: - percent = int(downloaded / total * 100) - if percent != progress['percent']: - progress['percent'] = percent - asyncio.run_coroutine_threadsafe(progress_callback(percent), loop) - elif d['status'] == 'finished': - asyncio.run_coroutine_threadsafe(progress_callback(100), loop) - - opts = { - 'format': format, - 'paths': {'home': out_path, 'temp': temp_path}, - 'progress_hooks': [hook] - } - with yt_dlp.YoutubeDL(opts) as ydl: - ydl.download([url]) - - return format \ No newline at end of file