Adds a floating button to YouTube pages to download audio as MP3 via a local server, with concurrent download support.
// ==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);
})();