iTunz

Docker compose

---
services:
  itunz:
    container_name: itunz
    build: 
      context: src/
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      - PUID=0
      - PGID=100
    volumes:
      - /dl:/app/downloads
    labels:
      - "com.centurylinklabs.watchtower.enable=false"

Dockerfile

FROM python:3.10

# Install dependencies
RUN apt update && apt install -y ffmpeg && rm -rf /var/lib/apt/lists/*
RUN pip install flask yt-dlp

# PORTS
EXPOSE 5000

# Set working directory
WORKDIR /app

# Copy codes
COPY app/ .

# Run flask app
CMD ["python", "app.py"]

Scripts

app.py

from flask import Flask, request, render_template
import subprocess
import os
import shutil

app = Flask(__name__)
DOWNLOAD_FOLDER = "downloads"
TMP_DOWNLOAD_FOLDER = "tmp_downloads"
os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
os.makedirs(TMP_DOWNLOAD_FOLDER, exist_ok=True)

@app.route('/')
def index():
    return render_template('index.html', status="")

@app.route('/download', methods=['POST'])
def download():
    url = request.form['url']
    output_template = os.path.join(
        TMP_DOWNLOAD_FOLDER,
        "%(artist)s/%(release_date>%Y)s - %(album)s/%(track_number)02d - %(title)s.%(ext)s"
    )

    command = [
        "yt-dlp", "-o", output_template,
        "-x", "--audio-format", "mp3", url
    ]
    result = subprocess.run(command, capture_output=True, text=True)

    downloaded_files = []
    for line in result.stdout.splitlines():
        if "Destination" in line:
            downloaded_files.append(line)

    if downloaded_files:
        move_downloaded_files()
        return render_template('index.html', status="Download complete ✅")

    return render_template('index.html', status=f"Download failed ❌: {result.stderr}")

def move_downloaded_files():
    """Moves everything from tmp_downloads/ to downloads/ recursively."""
    for directory in os.listdir(TMP_DOWNLOAD_FOLDER):
        src_path = os.path.join(TMP_DOWNLOAD_FOLDER, directory)
        dst_path = os.path.join(DOWNLOAD_FOLDER, directory)

        if os.path.exists(src_path):  # Ensure the source exists
            shutil.move(src_path, dst_path)  # Moves directories & files recursively

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MP3 Downloader</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            text-align: center;
            margin: 50px;
        }
        h1 {
            color: #333;
        }
        form {
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
            display: inline-block;
        }
        input {
            padding: 10px;
            width: 300px;
            margin: 10px 0;
            border: 1px solid #ccc;
            border-radius: 5px;
        }
        .button-container {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            margin-top: 10px;
        }
        button {
            background-color: #28a745;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        button:hover {
            background-color: #218838;
        }
        .status {
            margin-top: 20px;
            font-size: 18px;
            color: #333;
        }
        .spinner {
            display: none;
            width: 24px;
            height: 24px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <h1>MP3 Downloader</h1>
    <form id="download-form" action='/download' method='post'>
        <input type="text" name="url" placeholder="URL" required>
        <div class="button-container">
            <button type="submit" id="download-button">Download</button>
            <div class="spinner" id="spinner"></div>
        </div>
    </form>
    <div class="status">{{ status }}</div>

    <script>
        document.getElementById("download-form").addEventListener("submit", function() {
            // Hide button to show spinner
            document.getElementById("download-button").style.display = "none";
            document.getElementById("spinner").style.display = "block";
        });

        // Request permission to show notifications
        if (Notification.permission !== "granted") {
            Notification.requestPermission();
        }

        // Print notification while downloaded
        function showNotification(title, message) {
            if (Notification.permission === "granted") {
                new Notification(title, { body: message });
            }
        }

        // Check if download completed; if so, show notification
        {% if status %}
        showNotification("Download Completed", "{{ status }}");
        // Hide spinner to show button
        document.getElementById("download-button").style.display = "block";
        document.getElementById("spinner").style.display = "none";
        {% endif %}
    </script>
</body>
</html>