diff --git a/.github/workflows/submodule-check.yml b/.github/workflows/submodule-check.yml new file mode 100644 index 0000000..4acbe7d --- /dev/null +++ b/.github/workflows/submodule-check.yml @@ -0,0 +1,62 @@ +name: Submodule Cleanliness Check + +on: + pull_request: + branches: [ main, master ] + push: + branches: [ main, master ] + +jobs: + verify-submodules: + runs-on: ubuntu-latest + steps: + - name: Checkout repository with submodules + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Initialize/Update submodules + run: | + git submodule sync --recursive + git submodule update --init --recursive --jobs 4 + + - name: Ensure submodule status has no anomalies + run: | + set -euo pipefail + anomalies=0 + + echo "Checking git submodule status output" + status_output=$(git submodule status --recursive || true) + echo "$status_output" + + # If any line starts with '-', submodule is not initialized + if echo "$status_output" | grep -E '^-' >/dev/null; then + echo "ERROR: Found uninitialized submodules (lines starting with '-')" >&2 + anomalies=1 + fi + + # If any line starts with '+', submodule commit is different than recorded + if echo "$status_output" | grep -E '^\+' >/dev/null; then + echo "ERROR: Found submodules not at recorded commit (lines starting with '+')" >&2 + anomalies=1 + fi + + # If any line contains 'dirty', submodule has local changes + if echo "$status_output" | grep -E 'dirty' >/dev/null; then + echo "ERROR: Found dirty submodule working trees" >&2 + anomalies=1 + fi + + # Also ensure working tree under submodules is clean + if ! git diff --submodule=diff --quiet; then + echo "ERROR: Detected differences in submodules (working tree not clean)" >&2 + anomalies=1 + fi + + if [ "$anomalies" -ne 0 ]; then + echo "Submodule anomalies detected; failing the job." >&2 + exit 1 + fi + + echo "Submodule status clean." diff --git a/.gitignore b/.gitignore index 988fb45..46a967c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ -<<<<<<< HEAD build/bin -node_modules TODO.md +node_modules/ + -======= -fyne-cross -TODO.md ->>>>>>> 5c9da659bc26ab9faf682fb549dc7e0b599a7169 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bd6cab8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing + +## Submodules policy + +This repository includes a `frontend/` submodule. To maintain reproducible builds and CI integrity: + +- Never commit a dirty submodule state. Ensure `git submodule status` shows no `dirty` markers. +- Update submodules deterministically using: + - `git submodule sync --recursive` + - `git submodule update --init --recursive --checkout` +- When you change the `frontend/` code, commit and push inside the submodule first, then update the pointer in this repo: + 1. `cd frontend` + 2. Work and commit: `git add -A && git commit -m "feat: ..." && git push` + 3. `cd .. && git add frontend && git commit -m "chore(frontend): bump submodule" && git push` + +CI will block merges that contain uninitialized, mismatched, or dirty submodule pointers (see `.github/workflows/submodule-check.yml`). + +## Development quickstart + +- Clone with submodules: `git clone --recurse-submodules https://github.com/Aswanidev-vs/Predator.git` +- Or initialize after clone: `git submodule update --init --recursive` +- Go toolchain: see `go.mod` (`go 1.25`). Build/dev commands: + - Dev: `wails dev` + - Build: `wails build` diff --git a/README.md b/README.md index bd408b2..9ab3fd7 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,174 @@ # Predator

- +

-A simple desktop app for grabbing videos and audio from YouTube and Instagram. Built with Go and Wails (web tech frontend), using yt-dlp under the hood to do the heavy lifting. +

-## What it does +Release +

-- **Download videos** from YouTube in any resolution from 144p up to 4K -- **Grab audio only** in MP3, M4A, Opus, or WAV format -- **Works with Instagram too** - reels, posts, stories, TV videos -- **Playlist support** - Download entire YouTube playlists or pick specific videos -- **Shows file sizes** before you download so you know what you're getting into -- **Live progress tracking** - Watch speed, ETA, and percentage in real-time -- **Cancel anytime** - Change your mind? Hit cancel and it stops cleanly -- **Picks up where it left off** - Failed downloads auto-retry up to 3 times -- **Duplicate detection** - Warns if you already downloaded something -- **Choose where files go** - Set your download folder once, it remembers -- **Keeps history** - See what you've downloaded, filter by video/audio, open files directly -- **Dark and light themes** - Toggle with Ctrl+T +A simple desktop app for grabbing videos and audio from YouTube and Instagram. Built with Go and Wails (web tech frontend), using yt-dlp under the hood to do the heavy lifting. -## Getting started +## Table of Contents -You'll need **Go 1.24 or newer** installed. +- [Features](#features) +- [Getting Started](#getting-started) +- [How to Use](#how-to-use) +- [Keyboard Shortcuts](#keyboard-shortcuts) +- [Technical Details](#technical-details) +- [Requirements](#requirements) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) +- [License](#license) -```bash -git clone https://github.com/Aswanidev-vs/Predator.git -cd Predator -go mod download -``` +## Features -# Predator - YouTube Downloader -

- -

-A modern, cross-platform desktop application for downloading videos and audio from YouTube . -It is built using Go and the Fyne GUI framework, with yt-dlp as the download engine. +### Video Downloads +- **Multiple resolutions** - From 144p up to 4K (2160p) +- **Best quality option** - Automatically picks the highest available +- **Smart codec handling** - Uses H.264 video + AAC audio for maximum compatibility +- **WebM support** - Available with VP9/AV1 + Opus for better quality -The project focuses on simplicity, transparency, and responsive user experience while keeping the implementation clean and maintainable. +### Audio Downloads +- **Multiple formats** - MP3, M4A, Opus, or WAV +- **High quality output** - Uses best available source and converts to your chosen format +- **MP3**: V0 quality (~245kbps) +- **M4A**: AAC 192kbps +- **WAV**: 16-bit PCM 44.1kHz stereo -## Features +### Platform Support +- **YouTube** - Videos, shorts, live streams +- **Instagram** - Reels, posts, stories, TV videos -- **Video Download**: Download YouTube videos in various resolutions (144p to 2160p) -- **Audio Extraction**: Extract audio from videos in multiple formats (MP3, M4A, Opus, WAV) -- **Real-time Metadata**: Fetches video information dynamically, showing available resolutions with file sizes -- **Progress Tracking**: Live download progress ETA information -- **Cancellable Downloads**: Stop downloads at any time -- **Custom Output Directory**: Select and manage your download location -- **Cross-platform**: Works on Windows, macOS, and Linux +### User Experience +- **Playlist support** - Download entire playlists or select specific videos +- **File size preview** - See file sizes before downloading +- **Live progress** - Real-time speed, ETA, and percentage tracking +- **Cancel anytime** - Clean cancellation with automatic temp file cleanup +- **Auto-retry** - Failed downloads retry up to 3 times automatically +- **Duplicate detection** - Warns if content was already downloaded +- **Custom download folder** - Set once, remembers forever +- **Download history** - View past downloads, filter by type, open file location -## Requirements -- **Go**: 1.24.0 or higher -- **FFmpeg**: Required for audio extraction and video merging (automatically handled by yt-dlp) -- **yt-dlp**: Automatically installed on first run +## Getting Started -## Installation +You'll need **Go 1.24 or newer** installed. -### Clone the Repository ```bash +# Clone the repository git clone https://github.com/Aswanidev-vs/Predator.git cd Predator + +# Install dependencies +go mod download ``` -### Install Dependencies +### Running the App + +**Development mode:** ```bash -go mod download +wails dev ``` -### Build and Run +**Build for production:** ```bash -go run main.go +wails build ``` -### Build for Distribution -The project includes cross-platform build configuration in the `fyne-cross` directory: +**First time setup:** The app will ask to download yt-dlp and ffmpeg automatically. Just say yes - it handles everything and caches them locally. No need to install anything else manually. -```bash -# For Windows -go install fyne.io/tools/cmd/fyne-cross@latest -fyne-cross windows +## How to Use -# For macOS -fyne-cross darwin +1. **Open the app** - It'll show the main download tab +2. **Set your download folder** - Click "Change Download Location" (or it'll use the current folder) +3. **Paste a URL** - YouTube or Instagram links both work +4. **Pick your format:** + - For video: Choose resolution (144p to 4K, or "best") + - For audio: Pick format (MP3, M4A, Opus, WAV) +5. **Hit download** - Watch the progress bar do its thing -# For Linux -fyne-cross linux -``` -## Usage +**Pro tips:** +- Playlists show a modal where you can select specific videos to download +- Click the folder icon in history to open where a file is saved +- The app auto-updates yt-dlp to the latest version on each download -1. **Launch the Application**: Run the executable or `go run main.go` -2. **Set Download Location**: Click "Change Download Location" to select where videos will be saved -3. **Paste YouTube URL**: Paste a YouTube video link into the URL field -4. **Select Download Type**: - - **Video**: Choose a resolution and video will be downloaded with best audio merged - - **Audio**: Choose an audio format (MP3, M4A, Opus, WAV) -5. **Click Download**: Start the download process -6. **Monitor Progress**: Watch real-time progress, speed, and ETA -7. **Cancel if Needed**: Click Cancel button to stop an ongoing download +## Technical Details -## Architecture +### Video Format +- **Container:** MP4 (with WebM option) +- **Video codec:** H.264 (avc1) for maximum device compatibility +- **Audio codec:** AAC (m4a) for best audio quality +- **Fallback handling:** If preferred codec isn't available, automatically falls back to next best option -### Main Components +### Audio Format +- Uses best available source (webm/opus or m4a/aac) and converts to your chosen format using ffmpeg -- **UI Layer**: Built with [Fyne](https://fyne.io/) - a cross-platform GUI framework -- **Download Engine**: Powered by [go-ytdlp](https://github.com/lrstanley/go-ytdlp) - a Go wrapper for yt-dlp -- **Async Operations**: Uses goroutines for non-blocking UI interactions -- **Progress Tracking**: Real-time progress callbacks with ETA calculations +### File Locations +- **Downloads:** User-selected folder or `PREDATOR_OUTPUT_DIR` environment variable +- **History:** `~/.predator/history.json` (keeps last 100 items) +- **Settings:** `~/.predator/settings.json` +- **Dependencies:** `~/.cache/yt-dlp/` (yt-dlp, ffmpeg, ffprobe) -### Key Functions +### Performance +- **Concurrent downloads:** Max 3 at a time to not overwhelm your connection +- **Progress updates:** Every 200ms for smooth progress bars +- **Speed smoothing:** Uses exponential moving average for stable speed display -- `main()`: Initializes the UI and event handlers -- `fetchVideoInfo()`: Dynamically fetches video metadata and available resolutions -- `formatBytes()`: Converts byte sizes to human-readable format -- `formatETA()`: Converts duration to HH:MM:SS format +### Cleanup +- Cancelled downloads automatically clean up `.part` and temp files +- Failed downloads after max retries also trigger cleanup -## Project Structure +## Requirements -``` -Predator/ -├── main.go # Main application code -├── go.mod # Go module definition -├── go.sum # Go module checksums -├── logov4.png # Application icon -├── asset/ # Icon and logo assets -├── fyne-cross/ # Cross-platform build configuration -├── sample.txt # Code sample reference -└── samplev*.txt # Additional code samples -``` +- Go 1.24+ +- Internet connection +- yt-dlp and ffmpeg (auto-installed on first run) -## Dependencies +## Troubleshooting -- **fyne.io/fyne/v2**: Cross-platform GUI framework -- **github.com/lrstanley/go-ytdlp**: Go wrapper for yt-dlp +### "ffmpeg not found" error +The app will prompt you to download ffmpeg on first run. If you skip it, you can: +1. Install ffmpeg manually from [ffmpeg.org](https://ffmpeg.org) +2. Or delete `~/.cache/yt-dlp/` and restart the app - it'll ask again +### Slow downloads +- Check your internet connection +- YouTube might be throttling (try a different resolution) +- Max 3 concurrent downloads - close other apps using bandwidth -## Performance +### Download fails with "format not available" +- Try a lower resolution +- The video might be region-locked or unavailable +- Try using "best" option for auto-selection of best available -- **Lazy Metadata Loading**: Video info is only fetched when a valid URL is entered -- **Debounced Input**: 600ms debounce on URL input to prevent excessive API calls -- **Atomic Operations**: Thread-safe state management for concurrent operations -- **Streaming Download**: Supports live progress updates during download +### "Permission denied" errors +- Check if your download folder is writable +- Try a different download location -## Disclaimer +## Contributing -**IMPORTANT LEGAL NOTICE** +Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines. -This application is provided for educational and personal use only. Users are solely responsible for compliance with all applicable laws and regulations in their jurisdiction. +## Legal Stuff -### Legal Considerations +**This tool is for educational and personal use only.** -- **Copyright Compliance**: Users must respect copyright laws and YouTube's Terms of Service. Downloading copyrighted content without proper authorization may be illegal in your jurisdiction. -- **Terms of Service**: By using this application, you acknowledge that you are bound by YouTube's Terms of Service and agree not to violate them. -- **Personal Use Only**: This tool is intended for downloading content you own or have permission to download. -- **Liability**: The authors and contributors of Predator are not responsible for: - - Any copyright infringement or violations of third-party rights - - Misuse of downloaded content - - Any legal consequences arising from use of this application - - Data loss or corruption - - Any other damages resulting from use of this software +- Respect copyright laws and YouTube's Terms of Service +- Only download content you own or have permission to download +- Don't use this to redistribute copyrighted material +- The authors aren't responsible for how you use this tool -### Responsible Use +Basically: be cool, don't pirate stuff you shouldn't. -Users should: -- Only download content they have permission to download -- Respect content creators' rights and intellectual property -- Use downloaded content in compliance with local laws -- Not distribute copyrighted content without proper licensing -- Be aware that many YouTube videos are protected by copyright +## License -### No Warranty +MIT License - see [LICENSE](LICENSE) file. -This application 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, or non-infringement. +--- -The authors assume no responsibility for any illegal activities or misuse of this tool. By using Predator, you accept all risks and responsibilities associated with your downloads. +Built with [Wails](https://wails.io/) + Go + vanilla JS. No React, no bloat. diff --git a/app.go b/app.go index acf0d9b..0ed011a 100644 --- a/app.go +++ b/app.go @@ -412,78 +412,159 @@ func extractResolution(resolution string) string { /* -------------------- Format Builders -------------------- */ +// buildVideoFormatString builds format string for mp4 container with proper codec handling +// Uses separate video+audio selection to ensure reliable merging func buildVideoFormatString(cleanRes string, preferH264 bool) string { - // Build format string that handles all codecs (h264, vp9, av1) - // CRITICAL: Always use m4a audio for mp4 container compatibility - // webm/opus audio causes ffmpeg merge failures with mp4 video + // Build format string that prioritizes h264+aac for mp4 container compatibility + // Use separate selection (bv*+ba*) to ensure proper download and merging if cleanRes == "best" { - // Try h264 first, then vp9, then av1 - // Audio: STRICTLY m4a/aac only to ensure mp4 compatibility - return "bestvideo[vcodec^=avc1]+bestaudio[ext=m4a]/" + - "bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/" + - "bestvideo[vcodec^=vp9]+bestaudio[ext=m4a]/" + - "bestvideo[vcodec^=vp9]+bestaudio[acodec^=mp4a]/" + - "bestvideo[vcodec^=av01]+bestaudio[ext=m4a]/" + - "bestvideo[vcodec^=av01]+bestaudio[acodec^=mp4a]/" + - "bestvideo+bestaudio[ext=m4a]/" + - "bestvideo+bestaudio[acodec^=mp4a]/" + - "bestvideo+bestaudio/best" + // Best quality - prioritize h264 with aac/m4a for reliable mp4 merging + return "bestvideo[vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]/" + + "bestvideo[vcodec^=avc1]+bestaudio/" + + "bestvideo[vcodec^=vp9]+bestaudio[ext=m4a]/bestvideo[vcodec^=vp9]+bestaudio[acodec^=mp4a]/" + + "bestvideo[vcodec^=vp9]+bestaudio/" + + "bestvideo[vcodec^=av01]+bestaudio[ext=m4a]/bestvideo[vcodec^=av01]+bestaudio[acodec^=mp4a]/" + + "bestvideo[vcodec^=av01]+bestaudio/" + + "bestvideo+bestaudio[ext=m4a]/bestvideo+bestaudio[acodec^=mp4a]/bestvideo+bestaudio/best" } resNum, _ := strconv.Atoi(cleanRes) if resNum >= 1440 { - // High resolutions - try exact height first, then fall back to <= - // STRICTLY m4a audio only to prevent ffmpeg merge failures + // High resolutions (1440p+) - prioritize h264 with exact or close height match return fmt.Sprintf( - // Try exact height match first with m4a audio only + // h264 with m4a audio - best for mp4 merging "bestvideo[vcodec^=avc1][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=avc1][height=%s]+bestaudio/"+ + // h264 with <= height fallback + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[ext=m4a]/"+ + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+ + // vp9 fallback "bestvideo[vcodec^=vp9][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+ + // av1 fallback "bestvideo[vcodec^=av01][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=mp4a]/"+ - // Fall back to <= if exact match not available - still m4a only - "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+ - "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+ - "bestvideo[vcodec^=av01][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=mp4a]/"+ - // Final fallbacks - still prefer m4a + "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+ + // Any video codec fallback "bestvideo[height<=%s]+bestaudio[ext=m4a]/"+ "bestvideo[height<=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[height<=%s]+bestaudio/"+ "best[height<=%s]", cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, - cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, ) } - // Standard resolutions - try exact height first, then fall back - // STRICTLY m4a audio only to prevent ffmpeg merge failures + // Standard resolutions - prioritize h264 with m4a audio return fmt.Sprintf( - // Try exact height match first with m4a audio only + // h264 with m4a audio - best for mp4 merging "bestvideo[vcodec^=avc1][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=avc1][height=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=avc1][height=%s]+bestaudio/"+ + // h264 with <= height fallback + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[ext=m4a]/"+ + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=avc1][height<=%s]+bestaudio/"+ + // vp9 fallback "bestvideo[vcodec^=vp9][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+ + // av1 fallback "bestvideo[vcodec^=av01][height=%s]+bestaudio[ext=m4a]/"+ "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=mp4a]/"+ - // Fall back to <= if exact match not available - still m4a only - "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=avc1][height<=%s]+bestaudio[acodec^=mp4a]/"+ - "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=mp4a]/"+ - "bestvideo[vcodec^=av01][height<=%s]+bestaudio[ext=m4a]/"+ - "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=mp4a]/"+ - // Final fallbacks - still prefer m4a + "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+ + // Any video codec fallback "bestvideo[height<=%s]+bestaudio[ext=m4a]/"+ "bestvideo[height<=%s]+bestaudio[acodec^=mp4a]/"+ + "bestvideo[height<=%s]+bestaudio/"+ + "best[height<=%s]", + cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, + ) +} + +// buildWebmFormatString builds format string for webm container with VP9/AV1 + opus +// This ensures best quality webm output with modern codecs +func buildWebmFormatString(cleanRes string) string { + // For webm, we want VP9 or AV1 video with opus audio + // This provides better quality than older vp8/opus combinations + + if cleanRes == "best" { + // Best quality webm - prioritize av01 (AV1) then vp9 + return "bestvideo[vcodec^=av01]+bestaudio[acodec^=opus]/" + + "bestvideo[vcodec^=av01]+bestaudio[ext=webm]/" + + "bestvideo[vcodec^=av01]+bestaudio/" + + "bestvideo[vcodec^=vp9]+bestaudio[acodec^=opus]/" + + "bestvideo[vcodec^=vp9]+bestaudio[ext=webm]/" + + "bestvideo[vcodec^=vp9]+bestaudio/" + + "bestvideo[vcodec^=vp8]+bestaudio[acodec^=opus]/" + + "bestvideo[vcodec^=vp8]+bestaudio/" + + "bestvideo+bestaudio[acodec^=opus]/bestvideo+bestaudio/best" + } + + resNum, _ := strconv.Atoi(cleanRes) + if resNum >= 1440 { + // High resolutions - prioritize av01 then vp9 + return fmt.Sprintf( + // AV1 with opus - best quality + "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=av01][height=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+ + // AV1 <= height fallback + "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=av01][height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=av01][height<=%s]+bestaudio/"+ + // VP9 with opus + "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+ + // VP9 <= height fallback + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+ + // Any codec fallback + "bestvideo[height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[height<=%s]+bestaudio/"+ + "best[height<=%s]", + cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, + ) + } + + // Standard resolutions - prioritize av01 then vp9 with opus + return fmt.Sprintf( + // AV1 with opus + "bestvideo[vcodec^=av01][height=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=av01][height=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=av01][height=%s]+bestaudio/"+ + // AV1 <= height fallback + "bestvideo[vcodec^=av01][height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=av01][height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=av01][height<=%s]+bestaudio/"+ + // VP9 with opus + "bestvideo[vcodec^=vp9][height=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=vp9][height=%s]+bestaudio/"+ + // VP9 <= height fallback + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[vcodec^=vp9][height<=%s]+bestaudio/"+ + // Any codec fallback + "bestvideo[height<=%s]+bestaudio[acodec^=opus]/"+ + "bestvideo[height<=%s]+bestaudio[ext=webm]/"+ + "bestvideo[height<=%s]+bestaudio/"+ "best[height<=%s]", cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, cleanRes, - cleanRes, cleanRes, cleanRes, + cleanRes, cleanRes, cleanRes, cleanRes, ) }