YouTube MP3 Auto Downloader

Adds a floating button to YouTube pages to download audio as MP3 via a local server, with concurrent download support.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         YouTube MP3 Auto Downloader
// @namespace    Violentmonkey Scripts
// @version      3.0
// @description  Adds a floating button to YouTube pages to download audio as MP3 via a local server, with concurrent download support.
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    const FLASK_URL = "http://localhost:8888";

    // --- UI Elements ---

    // 1. Container for all our UI
    const uiContainer = document.createElement('div');
    uiContainer.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 9999;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 10px;
    `;

    // 2. Container for status notifications
    const statusContainer = document.createElement('div');
    statusContainer.id = 'yt-dl-status-container';
    statusContainer.style.cssText = `
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 8px;
    `;

    // 3. The main download button
    const mainBtn = document.createElement('button');
    mainBtn.id = 'yt-dl-main-btn';
    mainBtn.textContent = '下載 MP3';
    mainBtn.style.cssText = `
        background: #1DA1F2; /* Twitter Blue */
        color: white;
        border: none;
        border-radius: 50px;
        padding: 12px 20px;
        font-size: 16px;
        font-weight: bold;
        cursor: pointer;
        box-shadow: 0 4px 12px rgba(0,0,0,0.2);
        transition: transform 0.2s ease-in-out, background-color 0.2s;
        width: 150px; /* Fixed width for consistent look */
        text-align: center;
    `;

    mainBtn.onmouseover = () => { mainBtn.style.transform = 'scale(1.05)'; };
    mainBtn.onmouseout = () => { mainBtn.style.transform = 'scale(1)'; };

    // --- Logic ---

    function createStatusElement(text) {
        const el = document.createElement('div');
        el.textContent = text;

        el.style.cssText = `
            background-color: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 8px 15px;
            border-radius: 20px;
            font-size: 14px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            opacity: 0;
            transform: translateX(20px);
            transition: opacity 0.3s, transform 0.3s;
        `;
        statusContainer.appendChild(el);
        // Animate in
        setTimeout(() => {
            el.style.opacity = '1';
            el.style.transform = 'translateX(0)';
        }, 10);
        return el;
    }

    async function handleDownload(task_id, statusEl) {
        // Poll for status
        const pollInterval = setInterval(async () => {
            try {
                const statusResponse = await fetch(`${FLASK_URL}/status/${task_id}`);
                if (!statusResponse.ok) {
                    throw new Error(`狀態檢查失敗: ${statusResponse.statusText}`);
                }

                const { status, message: errorMessage } = await statusResponse.json();

                if (status === 'done') {
                    clearInterval(pollInterval);
                    statusEl.textContent = '準備下載...';
                    statusEl.style.backgroundColor = '#28a745'; // Green

                    // Fetch the actual file
                    const fileResponse = await fetch(`${FLASK_URL}/get-file/${task_id}`);
                    if (!fileResponse.ok) throw new Error(`獲取檔案失敗: ${await fileResponse.text()}`);

                    const disposition = fileResponse.headers.get('Content-Disposition');
                    let filename = 'audio.mp3';
                    if (disposition && disposition.includes('attachment')) {
                        const filenameStarMatch = /filename\*=(?:.+?''(.+))/.exec(disposition);
                        if (filenameStarMatch && filenameStarMatch[1]) {
                            filename = decodeURIComponent(filenameStarMatch[1]);
                        } else {
                            const filenameMatch = /filename="([^"]+)"/.exec(disposition);
                            if (filenameMatch && filenameMatch[1]) filename = decodeURIComponent(filenameMatch[1]);
                        }
                    }

                    const blob = await fileResponse.blob();
                    const downloadUrl = window.URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(downloadUrl);
                    a.remove();

                    statusEl.textContent = '下載完成!';
                    setTimeout(() => statusEl.remove(), 3000);

                } else if (status === 'error') {
                    clearInterval(pollInterval);
                    throw new Error(errorMessage || '伺服器發生未知錯誤。');
                }
            } catch (pollError) {
                clearInterval(pollInterval);
                console.error('Download failed:', pollError);
                statusEl.textContent = `失敗: ${pollError.message.substring(0, 50)}...`;
                statusEl.style.backgroundColor = '#dc3545'; // Red
                // Keep error message on screen longer
                setTimeout(() => statusEl.remove(), 10000);
            }
        }, 2000); // Poll every 2 seconds
    }

    mainBtn.onclick = async () => {
        const url = window.location.href;

        if (!url.includes('/watch?v=') && !url.includes('/shorts/')) {
            alert('請先進入一個 YouTube 影片或 Shorts 頁面。');
            return;
        }

        mainBtn.disabled = true;
        mainBtn.textContent = '請求中...';
        mainBtn.style.backgroundColor = '#ffc107'; // Yellow

        try {
            const startResponse = await fetch(`${FLASK_URL}/start-download?url=${encodeURIComponent(url)}`);
            if (!startResponse.ok) {
                const errorText = await startResponse.text();
                throw new Error(`啟動失敗: ${errorText}`);
            }
            const { task_id } = await startResponse.json();

            // Create a new status element for this task
            const statusEl = createStatusElement('處理中...');
            // Start polling for this specific task
            handleDownload(task_id, statusEl);

        } catch (initialError) {
            alert(`下載失敗!\n請確認伺服器已運行。\n\n錯誤: ${initialError.message}`);
        } finally {
            // Always re-enable the main button
            mainBtn.disabled = false;
            mainBtn.textContent = '下載 MP3';
            mainBtn.style.backgroundColor = '#1DA1F2';
        }
    };

    // Assemble the UI and add to the page
    uiContainer.appendChild(statusContainer);
    uiContainer.appendChild(mainBtn);
    document.body.appendChild(uiContainer);

})();