Chrunos commited on
Commit
c09c0d6
·
verified ·
1 Parent(s): 4e312f6

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +22 -0
  2. app.py +140 -0
  3. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /code
6
+
7
+ # Copy the dependencies file to the working directory
8
+ COPY ./requirements.txt /code/requirements.txt
9
+
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
12
+
13
+ # Copy the main application file
14
+ COPY ./app.py /code/app.py
15
+
16
+ # Expose the port that Gunicorn will run on
17
+ # Hugging Face Spaces expects the app to run on port 7860
18
+ EXPOSE 7860
19
+
20
+ # Define the command to run the application using Gunicorn
21
+ # This will be a robust server for your API
22
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
app.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import threading
4
+ import uuid
5
+ import requests
6
+ from flask import Flask, request, jsonify
7
+ from urllib.parse import quote
8
+
9
+ # Initialize Flask App
10
+ app = Flask(__name__)
11
+
12
+ # --- Secret Management ---
13
+ # Get the base domain of your downloader service from Hugging Face secrets.
14
+ # You must set a secret named 'YTDLP_DOMAIN' in your Space settings.
15
+ # The value should be, for example, 'https://ytdlp.online'
16
+ YTDLP_DOMAIN = os.environ.get("YTDLP_DOMAIN")
17
+
18
+ # In-memory "database" to store task status
19
+ tasks = {}
20
+ tasks_lock = threading.Lock()
21
+
22
+ def get_format_code(quality: str) -> str:
23
+ """Maps user-friendly quality names to yt-dlp format codes."""
24
+ quality_map = {
25
+ "480": "135+140",
26
+ "720": "136+140",
27
+ "1080": "137+140",
28
+ "best": "best",
29
+ }
30
+ return quality_map.get(quality, "best")
31
+
32
+ def process_video_task(task_id: str, video_url: str, quality: str):
33
+ """
34
+ This function runs in a background thread. It calls the external API,
35
+ processes the streaming response, extracts the download URL, and updates the task status.
36
+ """
37
+ if not YTDLP_DOMAIN:
38
+ with tasks_lock:
39
+ tasks[task_id]['status'] = 'error'
40
+ tasks[task_id]['message'] = "YTDLP_DOMAIN secret is not set in the Space settings."
41
+ return
42
+
43
+ found_url = None
44
+ try:
45
+ format_code = get_format_code(quality)
46
+ command = f"{video_url} -f {format_code}"
47
+ encoded_command = quote(command)
48
+
49
+ # The endpoint that provides the streaming response
50
+ stream_api_url = f"{YTDLP_DOMAIN}/stream?command={encoded_command}"
51
+
52
+ # Use stream=True to handle the server-sent event stream
53
+ with requests.get(stream_api_url, stream=True, timeout=300) as response:
54
+ response.raise_for_status()
55
+
56
+ # Process each line of the streaming response
57
+ for line_bytes in response.iter_lines():
58
+ if line_bytes:
59
+ line = line_bytes.decode('utf-8')
60
+
61
+ # Use regex to find the relative download link
62
+ match = re.search(r'href="(/download/download/[^"]+)"', line)
63
+ if match:
64
+ relative_url = match.group(1)
65
+ # Construct the final, absolute URL
66
+ final_stream_url = f"{YTDLP_DOMAIN}{relative_url}"
67
+ found_url = final_stream_url
68
+ # Once found, no need to process the rest of the stream
69
+ break
70
+
71
+ # After checking the whole stream, update the status
72
+ with tasks_lock:
73
+ if found_url:
74
+ tasks[task_id]['status'] = 'completed'
75
+ tasks[task_id]['result'] = found_url
76
+ else:
77
+ # If the loop finished but no URL was found
78
+ tasks[task_id]['status'] = 'error'
79
+ tasks[task_id]['message'] = "Command executed, but no download link was found in the response."
80
+
81
+ except requests.exceptions.RequestException as e:
82
+ with tasks_lock:
83
+ tasks[task_id]['status'] = 'error'
84
+ tasks[task_id]['message'] = str(e)
85
+
86
+
87
+ @app.route('/process', methods=['POST'])
88
+ def start_processing():
89
+ """
90
+ Endpoint to start a new video processing task.
91
+ Accepts a JSON payload with 'video_url' and 'quality'.
92
+ Returns a task_id immediately.
93
+ """
94
+ data = request.get_json()
95
+ if not data or 'video_url' not in data or 'quality' not in data:
96
+ return jsonify({"error": "Missing 'video_url' or 'quality' in request body"}), 400
97
+
98
+ video_url = data['video_url']
99
+ quality = data.get('quality', 'best')
100
+
101
+ task_id = str(uuid.uuid4())
102
+ with tasks_lock:
103
+ tasks[task_id] = {'status': 'processing'}
104
+
105
+ thread = threading.Thread(target=process_video_task, args=(task_id, video_url, quality))
106
+ thread.daemon = True
107
+ thread.start()
108
+
109
+ return jsonify({'task_id': task_id}), 202
110
+
111
+ @app.route('/api/<string:task_id>', methods=['GET'])
112
+ def get_task_status(task_id: str):
113
+ """
114
+ Endpoint to check the status and result of a task.
115
+ Returns the task's current state. If completed, includes the final URL.
116
+ """
117
+ with tasks_lock:
118
+ task = tasks.get(task_id)
119
+
120
+ if task is None:
121
+ return jsonify({'error': 'Task not found'}), 404
122
+
123
+ # If the task completed but has no 'result' key, it's an error
124
+ if task['status'] == 'completed' and 'result' not in task:
125
+ return jsonify({
126
+ 'status': 'error',
127
+ 'message': 'Task completed but no result URL was generated.'
128
+ }), 500
129
+
130
+ return jsonify(task)
131
+
132
+ @app.route('/')
133
+ def index():
134
+ """A simple index route to show the API is running."""
135
+ if not YTDLP_DOMAIN:
136
+ return "<h1>Video Processing API Error</h1><p>Server is missing the YTDLP_DOMAIN secret.</p>", 500
137
+ return "<h1>Video Processing API is running!</h1><p>Use /process and /api/&lt;task_id&gt; endpoints.</p>"
138
+
139
+ if __name__ == '__main__':
140
+ app.run(debug=True, host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask==3.0.3
2
+ requests==2.32.3
3
+ gunicorn==22.0.0