devusman commited on
Commit
6ade7fe
·
1 Parent(s): 0195a63

feat: deployment

Browse files
Files changed (4) hide show
  1. Dockerfile +29 -0
  2. app.py +140 -0
  3. requirements.txt +3 -0
  4. templates/index.html +270 -0
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ # The two following lines are requirements for the Dev Mode to be functional
7
+ # Learn more about the Dev Mode at https://huggingface.co/dev-mode-explorers
8
+ RUN useradd -m -u 1000 user
9
+ WORKDIR /app
10
+
11
+ # Copy and install requirements
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ # Copy the application files
16
+ COPY --chown=user . /app
17
+
18
+ # Switch to the non-root user
19
+ USER user
20
+
21
+ # Set environment variables for the user
22
+ ENV HOME=/home/user \
23
+ PATH=/home/user/.local/bin:$PATH
24
+
25
+ # Expose the port Hugging Face Spaces expects
26
+ EXPOSE 7860
27
+
28
+ # Run the app with Gunicorn for production
29
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, send_file, jsonify, after_this_request
2
+ import yt_dlp
3
+ import io
4
+ import tempfile
5
+ import os
6
+ import threading
7
+ import uuid
8
+ import glob
9
+ import shutil
10
+
11
+ app = Flask(__name__)
12
+
13
+ tasks = {}
14
+
15
+ def progress_update(d, task_id):
16
+ if d['status'] == 'downloading':
17
+ total_bytes = d.get('total_bytes') or d.get('total_bytes_estimate')
18
+ if total_bytes:
19
+ percent = (d['downloaded_bytes'] / total_bytes) * 100
20
+ tasks[task_id]['progress'] = min(100, percent)
21
+ elif d['status'] == 'finished':
22
+ tasks[task_id]['progress'] = 100
23
+
24
+ @app.route('/')
25
+ def index():
26
+ """Renders the main page."""
27
+ return render_template('index.html')
28
+
29
+ @app.route('/download', methods=['POST'])
30
+ def download():
31
+ """Handles the video download request."""
32
+ video_url = request.form.get('url')
33
+
34
+ if not video_url:
35
+ return jsonify({'error': 'Please provide a video URL.'}), 400
36
+
37
+ task_id = str(uuid.uuid4())
38
+
39
+ try:
40
+ # Get info first
41
+ ydl_opts_info = {
42
+ 'noplaylist': True,
43
+ }
44
+ with yt_dlp.YoutubeDL(ydl_opts_info) as ydl:
45
+ info_dict = ydl.extract_info(video_url, download=False)
46
+ video_title = info_dict.get('title', 'video')
47
+ video_ext = info_dict.get('ext', 'mp4')
48
+ filename = f"{video_title}.{video_ext}"
49
+
50
+ tasks[task_id] = {
51
+ 'progress': 0,
52
+ 'done': False,
53
+ 'error': None,
54
+ 'filepath': None,
55
+ 'filename': filename,
56
+ 'tmpdir': None
57
+ }
58
+
59
+ def download_in_thread():
60
+ try:
61
+ tmpdir = tempfile.mkdtemp()
62
+ tasks[task_id]['tmpdir'] = tmpdir
63
+ ydl_opts = {
64
+ 'format': 'best',
65
+ 'outtmpl': os.path.join(tmpdir, '%(title)s.%(ext)s'),
66
+ 'noplaylist': True,
67
+ 'progress_hooks': [lambda d: progress_update(d, task_id)]
68
+ }
69
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl_download:
70
+ ydl_download.download([video_url])
71
+
72
+ # Find the downloaded file
73
+ files = glob.glob(os.path.join(tmpdir, '*'))
74
+ if files:
75
+ # Assume the first (or only) file is the final one; in practice, sort by name or check size
76
+ filepath = max(files, key=os.path.getsize) # largest file as final
77
+ tasks[task_id]['filepath'] = filepath
78
+ actual_filename = os.path.basename(filepath)
79
+ tasks[task_id]['filename'] = actual_filename
80
+ tasks[task_id]['progress'] = 100
81
+ tasks[task_id]['done'] = True
82
+ except Exception as e:
83
+ tasks[task_id]['error'] = f'An error occurred during download: {str(e)}'
84
+ tasks[task_id]['done'] = True
85
+
86
+ thread = threading.Thread(target=download_in_thread, daemon=True)
87
+ thread.start()
88
+
89
+ return jsonify({'task_id': task_id})
90
+
91
+ except Exception as e:
92
+ return jsonify({'error': f'An unexpected error occurred: {str(e)}'}), 500
93
+
94
+ @app.route('/progress/<task_id>')
95
+ def get_progress(task_id):
96
+ """Get progress for a task."""
97
+ task = tasks.get(task_id)
98
+ if not task:
99
+ return jsonify({'error': 'Task not found'}), 404
100
+ return jsonify({
101
+ 'progress': task['progress'],
102
+ 'done': task['done'],
103
+ 'error': task['error'],
104
+ 'filename': task.get('filename')
105
+ })
106
+
107
+ @app.route('/file/<task_id>')
108
+ def get_file(task_id):
109
+ """Serve the downloaded file."""
110
+ task = tasks.get(task_id)
111
+ if not task or not task['done'] or task['error'] or not task['filepath']:
112
+ if task and task['error']:
113
+ return jsonify({'error': task['error']}), 500
114
+ return jsonify({'error': 'Download not ready or failed'}), 404
115
+
116
+ try:
117
+ ext = task['filepath'].split('.')[-1]
118
+ @after_this_request
119
+ def cleanup(response):
120
+ try:
121
+ os.unlink(task['filepath'])
122
+ if task.get('tmpdir'):
123
+ shutil.rmtree(task['tmpdir'])
124
+ except Exception:
125
+ pass
126
+ if task_id in tasks:
127
+ del tasks[task_id]
128
+ return response
129
+
130
+ return send_file(
131
+ task['filepath'],
132
+ as_attachment=True,
133
+ download_name=task['filename'],
134
+ mimetype=f'video/{ext}'
135
+ )
136
+ except Exception as e:
137
+ return jsonify({'error': f'An unexpected error occurred: {str(e)}'}), 500
138
+
139
+ if __name__ == '__main__':
140
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask
2
+ yt-dlp
3
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Video Downloader</title>
7
+
8
+ <style>
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
11
+ Helvetica, Arial, sans-serif;
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ height: 100vh;
16
+ margin: 0;
17
+ background-color: #f4f7f6;
18
+ }
19
+
20
+ .container {
21
+ background: white;
22
+ padding: 2rem;
23
+ border-radius: 8px;
24
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
25
+ width: 100%;
26
+ max-width: 500px;
27
+ text-align: center;
28
+ }
29
+
30
+ h1 {
31
+ margin-bottom: 1.5rem;
32
+ color: #333;
33
+ }
34
+
35
+ #download-form {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 1rem;
39
+ }
40
+
41
+ #url {
42
+ padding: 0.75rem;
43
+ border: 1px solid #ccc;
44
+ border-radius: 4px;
45
+ font-size: 1rem;
46
+ }
47
+
48
+ button {
49
+ padding: 0.75rem;
50
+ border: none;
51
+ border-radius: 4px;
52
+ background-color: #007bff;
53
+ color: white;
54
+ font-size: 1rem;
55
+ cursor: pointer;
56
+ transition: background-color 0.2s;
57
+ }
58
+
59
+ button:hover {
60
+ background-color: #0056b3;
61
+ }
62
+
63
+ button:disabled {
64
+ background-color: #6c757d;
65
+ cursor: not-allowed;
66
+ }
67
+
68
+ .message {
69
+ margin-top: 1rem;
70
+ padding: 0.75rem;
71
+ border-radius: 4px;
72
+ display: none;
73
+ }
74
+
75
+ .message.info {
76
+ display: block;
77
+ background-color: #e2f3ff;
78
+ color: #005f9e;
79
+ }
80
+
81
+ .message.success {
82
+ display: block;
83
+ background-color: #d4edda;
84
+ color: #155724;
85
+ }
86
+
87
+ .message.error {
88
+ display: block;
89
+ background-color: #f8d7da;
90
+ color: #721c24;
91
+ }
92
+
93
+ #progress {
94
+ display: none;
95
+ margin-top: 1rem;
96
+ }
97
+
98
+ #progress-bar {
99
+ width: 100%;
100
+ height: 20px;
101
+ }
102
+
103
+ #percent {
104
+ text-align: center;
105
+ margin-top: 0.5rem;
106
+ font-weight: bold;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <div class="container">
112
+ <h1>Video Downloader</h1>
113
+ <form id="download-form">
114
+ <input
115
+ type="text"
116
+ id="url"
117
+ name="url"
118
+ placeholder="Enter video URL"
119
+ required
120
+ />
121
+ <button type="submit" id="download-btn">Download</button>
122
+ </form>
123
+ <div id="progress">
124
+ <progress id="progress-bar" value="0" max="100"></progress>
125
+ <div id="percent">0%</div>
126
+ </div>
127
+ <div id="message" class="message"></div>
128
+ </div>
129
+
130
+ <script>
131
+ document
132
+ .getElementById("download-form")
133
+ .addEventListener("submit", function (event) {
134
+ event.preventDefault();
135
+
136
+ const form = event.target;
137
+ const url = form.elements.url.value;
138
+ const messageDiv = document.getElementById("message");
139
+ const progressDiv = document.getElementById("progress");
140
+ const progressBar = document.getElementById("progress-bar");
141
+ const percentSpan = document.getElementById("percent");
142
+ const downloadBtn = document.getElementById("download-btn");
143
+
144
+ // Reset
145
+ messageDiv.style.display = "none";
146
+ progressDiv.style.display = "none";
147
+ downloadBtn.disabled = true;
148
+
149
+ // Start
150
+ messageDiv.textContent = "Starting download...";
151
+ messageDiv.className = "message info";
152
+ messageDiv.style.display = "block";
153
+ progressDiv.style.display = "block";
154
+ progressBar.value = 0;
155
+ percentSpan.textContent = "0%";
156
+
157
+ const formData = new FormData();
158
+ formData.append("url", url);
159
+
160
+ const postXhr = new XMLHttpRequest();
161
+ postXhr.open("POST", "/download", true);
162
+
163
+ postXhr.addEventListener("load", function () {
164
+ if (postXhr.status === 200) {
165
+ try {
166
+ const data = JSON.parse(postXhr.responseText);
167
+ const taskId = data.task_id;
168
+
169
+ messageDiv.textContent = "Downloading video...";
170
+ messageDiv.className = "message info";
171
+
172
+ let polling = true;
173
+
174
+ function poll() {
175
+ if (!polling) return;
176
+
177
+ const getXhr = new XMLHttpRequest();
178
+ getXhr.open("GET", `/progress/${taskId}`, true);
179
+
180
+ getXhr.addEventListener("load", function () {
181
+ if (getXhr.status === 200) {
182
+ try {
183
+ const task = JSON.parse(getXhr.responseText);
184
+ const percent = task.progress || 0;
185
+ progressBar.value = percent;
186
+ percentSpan.textContent = Math.round(percent) + "%";
187
+
188
+ if (task.done) {
189
+ polling = false;
190
+ progressDiv.style.display = "none";
191
+ if (task.error) {
192
+ messageDiv.textContent = task.error;
193
+ messageDiv.className = "message error";
194
+ } else {
195
+ messageDiv.textContent =
196
+ "Download completed successfully!";
197
+ messageDiv.className = "message success";
198
+
199
+ const filename =
200
+ task.filename || "downloaded_video.mp4";
201
+ const link = document.createElement("a");
202
+ link.href = `/file/${taskId}`;
203
+ link.download = filename;
204
+ document.body.appendChild(link);
205
+ link.click();
206
+ document.body.removeChild(link);
207
+ }
208
+ downloadBtn.disabled = false;
209
+ } else {
210
+ setTimeout(poll, 500);
211
+ }
212
+ } catch (e) {
213
+ // Parse error
214
+ polling = false;
215
+ messageDiv.textContent =
216
+ "Error parsing progress update.";
217
+ messageDiv.className = "message error";
218
+ downloadBtn.disabled = false;
219
+ }
220
+ } else {
221
+ // Poll error, retry
222
+ setTimeout(poll, 1000);
223
+ }
224
+ });
225
+
226
+ getXhr.addEventListener("error", function () {
227
+ polling = false;
228
+ messageDiv.textContent =
229
+ "Network error while checking progress.";
230
+ messageDiv.className = "message error";
231
+ downloadBtn.disabled = false;
232
+ });
233
+
234
+ getXhr.send();
235
+ }
236
+
237
+ poll();
238
+ } catch (e) {
239
+ messageDiv.textContent = "Error starting download.";
240
+ messageDiv.className = "message error";
241
+ downloadBtn.disabled = false;
242
+ }
243
+ } else {
244
+ let errorMsg = `Error: ${postXhr.statusText}`;
245
+ try {
246
+ const errorData = JSON.parse(postXhr.responseText);
247
+ errorMsg = `Error: ${errorData.error}`;
248
+ } catch (e) {
249
+ // Ignore
250
+ }
251
+ messageDiv.textContent = errorMsg;
252
+ messageDiv.className = "message error";
253
+ progressDiv.style.display = "none";
254
+ downloadBtn.disabled = false;
255
+ }
256
+ });
257
+
258
+ postXhr.addEventListener("error", function () {
259
+ messageDiv.textContent =
260
+ "An unexpected error occurred: Network issue";
261
+ messageDiv.className = "message error";
262
+ progressDiv.style.display = "none";
263
+ downloadBtn.disabled = false;
264
+ });
265
+
266
+ postXhr.send(formData);
267
+ });
268
+ </script>
269
+ </body>
270
+ </html>