Youssouf ⚜️ commited on
Commit
ed98528
·
1 Parent(s): c63a7cd

feat: add YouTube to MP3 downloader with playlist support

Browse files
Files changed (4) hide show
  1. Dockerfile +2 -0
  2. app.py +142 -0
  3. index.html +218 -0
  4. requirements.txt +3 -0
Dockerfile CHANGED
@@ -3,6 +3,8 @@
3
 
4
  FROM python:3.9
5
 
 
 
6
  RUN useradd -m -u 1000 user
7
  USER user
8
  ENV PATH="/home/user/.local/bin:$PATH"
 
3
 
4
  FROM python:3.9
5
 
6
+ RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
7
+
8
  RUN useradd -m -u 1000 user
9
  USER user
10
  ENV PATH="/home/user/.local/bin:$PATH"
app.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import zipfile
5
+ import threading
6
+ import time
7
+ from flask import Flask, request, jsonify, send_file
8
+ from yt_dlp import YoutubeDL
9
+
10
+ app = Flask(__name__)
11
+
12
+ TASKS_DIR = "tasks"
13
+ if not os.path.exists(TASKS_DIR):
14
+ os.makedirs(TASKS_DIR)
15
+
16
+
17
+ def download_youtube(url, task_id):
18
+ task_file = os.path.join(TASKS_DIR, f"{task_id}.json")
19
+
20
+ def update_status(status, progress=None, total=None, error=None):
21
+ with open(task_file, "w") as f:
22
+ json.dump(
23
+ {
24
+ "status": status,
25
+ "progress": progress,
26
+ "total": total,
27
+ "error": error,
28
+ "url": url,
29
+ },
30
+ f,
31
+ )
32
+
33
+ try:
34
+ ydl_opts = {
35
+ "format": "bestaudio/best",
36
+ "outtmpl": os.path.join(TASKS_DIR, f"{task_id}/%(title)s.%(ext)s"),
37
+ "postprocessors": [
38
+ {
39
+ "key": "FFmpegExtractAudio",
40
+ "preferredcodec": "mp3",
41
+ "preferredquality": "192",
42
+ }
43
+ ],
44
+ "quiet": True,
45
+ "no_warnings": True,
46
+ "extract_flat": "in_playlist",
47
+ "max_downloads": 50,
48
+ }
49
+
50
+ update_status("analyzing")
51
+
52
+ with YoutubeDL(ydl_opts) as ydl:
53
+ info = ydl.extract_info(url, download=False)
54
+
55
+ if "entries" in info:
56
+ total = len(info["entries"][:50])
57
+ update_status("downloading", progress=0, total=total)
58
+
59
+ for idx, entry in enumerate(info["entries"][:50]):
60
+ video_url = entry["url"]
61
+ video_opts = ydl_opts.copy()
62
+ video_opts["outtmpl"] = os.path.join(
63
+ TASKS_DIR, f"{task_id}/%(title)s.%(ext)s"
64
+ )
65
+
66
+ with YoutubeDL(video_opts) as video_ydl:
67
+ video_ydl.download([video_url])
68
+
69
+ update_status("downloading", progress=idx + 1, total=total)
70
+ else:
71
+ update_status("downloading", progress=1, total=1)
72
+ ydl_opts["outtmpl"] = os.path.join(
73
+ TASKS_DIR, f"{task_id}/%(title)s.%(ext)s"
74
+ )
75
+ with YoutubeDL(ydl_opts) as ydl_single:
76
+ ydl_single.download([url])
77
+
78
+ zip_path = os.path.join(TASKS_DIR, f"{task_id}.zip")
79
+ with zipfile.ZipFile(zip_path, "w") as zipf:
80
+ for root, dirs, files in os.walk(os.path.join(TASKS_DIR, task_id)):
81
+ for file in files:
82
+ if file.endswith(".mp3"):
83
+ file_path = os.path.join(root, file)
84
+ arcname = file
85
+ zipf.write(file_path, arcname)
86
+
87
+ update_status("completed")
88
+
89
+ except Exception as e:
90
+ update_status("error", error=str(e))
91
+
92
+
93
+ @app.route("/download", methods=["POST"])
94
+ def start_download():
95
+ data = request.get_json()
96
+ url = data.get("url")
97
+
98
+ if not url:
99
+ return jsonify({"error": "URL required"}), 400
100
+
101
+ task_id = str(uuid.uuid4())
102
+ os.makedirs(os.path.join(TASKS_DIR, task_id), exist_ok=True)
103
+
104
+ thread = threading.Thread(target=download_youtube, args=(url, task_id))
105
+ thread.start()
106
+
107
+ return jsonify({"task_id": task_id})
108
+
109
+
110
+ @app.route("/status/<task_id>", methods=["GET"])
111
+ def get_status(task_id):
112
+ task_file = os.path.join(TASKS_DIR, f"{task_id}.json")
113
+
114
+ if not os.path.exists(task_file):
115
+ return jsonify({"error": "Task not found"}), 404
116
+
117
+ with open(task_file, "r") as f:
118
+ data = json.load(f)
119
+
120
+ return jsonify(data)
121
+
122
+
123
+ @app.route("/download/<task_id>", methods=["GET"])
124
+ def download_zip(task_id):
125
+ task_file = os.path.join(TASKS_DIR, f"{task_id}.json")
126
+
127
+ if not os.path.exists(task_file):
128
+ return jsonify({"error": "Task not found"}), 404
129
+
130
+ with open(task_file, "r") as f:
131
+ data = json.load(f)
132
+
133
+ if data["status"] != "completed":
134
+ return jsonify({"error": "Download not completed"}), 400
135
+
136
+ zip_path = os.path.join(TASKS_DIR, f"{task_id}.zip")
137
+
138
+ return send_file(zip_path, as_attachment=True, download_name="youtube_mp3.zip")
139
+
140
+
141
+ if __name__ == "__main__":
142
+ app.run(host="0.0.0.0", port=7860)
index.html ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>YouTube to MP3</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 600px;
11
+ margin: 50px auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .container {
16
+ background: white;
17
+ padding: 30px;
18
+ border-radius: 10px;
19
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
20
+ }
21
+ h1 {
22
+ color: #333;
23
+ margin-bottom: 30px;
24
+ }
25
+ input[type="url"] {
26
+ width: 100%;
27
+ padding: 12px;
28
+ border: 1px solid #ddd;
29
+ border-radius: 5px;
30
+ margin-bottom: 15px;
31
+ box-sizing: border-box;
32
+ }
33
+ button {
34
+ background: #ff0000;
35
+ color: white;
36
+ padding: 12px 30px;
37
+ border: none;
38
+ border-radius: 5px;
39
+ cursor: pointer;
40
+ font-size: 16px;
41
+ width: 100%;
42
+ }
43
+ button:hover {
44
+ background: #cc0000;
45
+ }
46
+ button:disabled {
47
+ background: #ccc;
48
+ cursor: not-allowed;
49
+ }
50
+ #status {
51
+ margin-top: 20px;
52
+ padding: 15px;
53
+ border-radius: 5px;
54
+ display: none;
55
+ }
56
+ .analyzing { background: #fff3cd; }
57
+ .downloading { background: #d1ecf1; }
58
+ .completed { background: #d4edda; }
59
+ .error { background: #f8d7da; }
60
+ #progress {
61
+ margin-top: 10px;
62
+ font-weight: bold;
63
+ }
64
+ .task-list {
65
+ margin-top: 30px;
66
+ }
67
+ .task-item {
68
+ background: #f9f9f9;
69
+ padding: 15px;
70
+ border-radius: 5px;
71
+ margin-bottom: 10px;
72
+ cursor: pointer;
73
+ }
74
+ .task-item:hover {
75
+ background: #eee;
76
+ }
77
+ .task-url {
78
+ font-size: 14px;
79
+ color: #666;
80
+ margin-bottom: 5px;
81
+ overflow: hidden;
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
85
+ .task-status {
86
+ font-weight: bold;
87
+ margin-bottom: 5px;
88
+ }
89
+ .task-completed { color: #28a745; }
90
+ .task-downloading { color: #17a2b8; }
91
+ .task-error { color: #dc3545; }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="container">
96
+ <h1>🎵 YouTube to MP3</h1>
97
+ <p>Entrez une URL YouTube (vidéo ou playlist)</p>
98
+
99
+ <input type="url" id="urlInput" placeholder="https://www.youtube.com/watch?v=..." />
100
+ <button onclick="startDownload()">Télécharger</button>
101
+
102
+ <div id="status"></div>
103
+ <div id="progress"></div>
104
+
105
+ <div class="task-list">
106
+ <h3>Tâches récentes</h3>
107
+ <div id="tasksList"></div>
108
+ </div>
109
+ </div>
110
+
111
+ <script>
112
+ let currentTaskId = null;
113
+ let tasks = JSON.parse(localStorage.getItem('tasks') || '[]');
114
+
115
+ function saveTasks() {
116
+ localStorage.setItem('tasks', JSON.stringify(tasks));
117
+ }
118
+
119
+ function renderTasks() {
120
+ const list = document.getElementById('tasksList');
121
+ list.innerHTML = '';
122
+
123
+ tasks.slice().reverse().forEach(task => {
124
+ const div = document.createElement('div');
125
+ div.className = 'task-item';
126
+ div.onclick = () => checkStatus(task.id);
127
+
128
+ let statusClass = '';
129
+ let statusText = '';
130
+
131
+ if (task.status === 'completed') {
132
+ statusClass = 'task-completed';
133
+ statusText = '✓ Terminé';
134
+ } else if (task.status === 'downloading' || task.status === 'analyzing') {
135
+ statusClass = 'task-downloading';
136
+ statusText = `⏳ ${task.progress}/${task.total}`;
137
+ } else if (task.status === 'error') {
138
+ statusClass = 'task-error';
139
+ statusText = '✗ Erreur';
140
+ }
141
+
142
+ div.innerHTML = `
143
+ <div class="task-status ${statusClass}">${statusText}</div>
144
+ <div class="task-url">${task.url}</div>
145
+ `;
146
+ list.appendChild(div);
147
+ });
148
+ }
149
+
150
+ async function startDownload() {
151
+ const url = document.getElementById('urlInput').value;
152
+
153
+ if (!url) {
154
+ alert('Veuillez entrer une URL');
155
+ return;
156
+ }
157
+
158
+ const response = await fetch('/download', {
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({ url })
162
+ });
163
+
164
+ const data = await response.json();
165
+
166
+ if (data.error) {
167
+ alert(data.error);
168
+ return;
169
+ }
170
+
171
+ currentTaskId = data.task_id;
172
+ tasks.push({ id: currentTaskId, url: url, status: 'analyzing', progress: 0, total: 0 });
173
+ saveTasks();
174
+ renderTasks();
175
+
176
+ checkStatus(currentTaskId);
177
+ }
178
+
179
+ async function checkStatus(taskId) {
180
+ currentTaskId = taskId;
181
+ const response = await fetch(`/status/${taskId}`);
182
+ const data = await response.json();
183
+
184
+ const statusDiv = document.getElementById('status');
185
+ const progressDiv = document.getElementById('progress');
186
+
187
+ statusDiv.style.display = 'block';
188
+ statusDiv.className = data.status;
189
+
190
+ if (data.status === 'analyzing') {
191
+ statusDiv.textContent = '📊 Analyse de la playlist...';
192
+ } else if (data.status === 'downloading') {
193
+ statusDiv.textContent = '📥 Téléchargement en cours...';
194
+ progressDiv.textContent = `${data.progress}/${data.total} vidéos`;
195
+ } else if (data.status === 'completed') {
196
+ statusDiv.textContent = '✅ Téléchargement terminé!';
197
+ progressDiv.innerHTML = `<a href="/download/${taskId}" style="color: #007bff;">📦 Télécharger le ZIP</a>`;
198
+ } else if (data.status === 'error') {
199
+ statusDiv.textContent = '❌ Erreur: ' + data.error;
200
+ progressDiv.textContent = '';
201
+ }
202
+
203
+ const taskIndex = tasks.findIndex(t => t.id === taskId);
204
+ if (taskIndex !== -1) {
205
+ tasks[taskIndex] = { ...tasks[taskIndex], status: data.status, progress: data.progress, total: data.total };
206
+ saveTasks();
207
+ renderTasks();
208
+ }
209
+
210
+ if (data.status === 'downloading' || data.status === 'analyzing') {
211
+ setTimeout(() => checkStatus(taskId), 2000);
212
+ }
213
+ }
214
+
215
+ renderTasks();
216
+ </script>
217
+ </body>
218
+ </html>
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask
2
+ yt-dlp
3
+ ffmpeg-python