kavion commited on
Commit
f9edca2
·
unverified ·
1 Parent(s): a10d715

Refactor: Split app.py into modular files

Browse files
Files changed (5) hide show
  1. app.py +6 -207
  2. cache.py +43 -0
  3. config.py +40 -0
  4. extractor.py +94 -0
  5. utils.py +18 -0
app.py CHANGED
@@ -1,215 +1,14 @@
1
- import json
2
  import os
3
  import re
4
- import subprocess
5
  import time
6
- import uuid
7
- from flask import Flask, request, make_response, send_file
8
-
9
- app = Flask(__name__)
10
-
11
- # ========================================
12
- # Konfigurasi temp file
13
- # ========================================
14
- TEMP_DIR = "/tmp/ytdlp_cache"
15
- EXPIRE_SECONDS = 6 * 3600 # 6 jam
16
- os.makedirs(TEMP_DIR, exist_ok=True)
17
-
18
-
19
- def jsonify(data, status=200):
20
- """Custom jsonify dengan indent 2 dan ensure_ascii False."""
21
- response = make_response(json.dumps(data, indent=2, ensure_ascii=False), status)
22
- response.headers["Content-Type"] = "application/json; charset=utf-8"
23
- return response
24
 
 
 
 
 
25
 
26
- # ========================================
27
- # Filesystem cache — aman lintas worker gunicorn
28
- # ========================================
29
-
30
- def meta_path(file_id):
31
- return os.path.join(TEMP_DIR, f"{file_id}.json")
32
-
33
- def video_path(file_id):
34
- return os.path.join(TEMP_DIR, f"{file_id}.mp4")
35
-
36
- def save_meta(file_id, filename, expires_at):
37
- """Simpan metadata file ke disk."""
38
- with open(meta_path(file_id), "w") as f:
39
- json.dump({"filename": filename, "expires_at": expires_at}, f)
40
-
41
- def load_meta(file_id):
42
- """Baca metadata file dari disk. Return None jika tidak ada."""
43
- p = meta_path(file_id)
44
- if not os.path.exists(p):
45
- return None
46
- try:
47
- with open(p) as f:
48
- return json.load(f)
49
- except Exception:
50
- return None
51
-
52
- def cleanup_expired():
53
- """Hapus file temp (video + meta) yang sudah expired."""
54
- now = time.time()
55
- for fname in os.listdir(TEMP_DIR):
56
- if not fname.endswith(".json"):
57
- continue
58
- fid = fname[:-5]
59
- meta = load_meta(fid)
60
- if meta and meta["expires_at"] < now:
61
- try:
62
- vp = video_path(fid)
63
- if os.path.exists(vp):
64
- os.remove(vp)
65
- os.remove(meta_path(fid))
66
- except Exception:
67
- pass
68
-
69
-
70
- # ========================================
71
- # Daftar API yang tersedia
72
- # ========================================
73
- API_LIST = {
74
- "name": "yt-dlp API",
75
- "version": "1.0.0",
76
- "description": "API untuk mengunduh dan mengekstrak informasi media dari berbagai platform menggunakan yt-dlp.",
77
- "endpoints": {
78
- "/api/tiktok": {
79
- "method": "GET",
80
- "description": (
81
- "Mengekstrak info video TikTok sekaligus mendownload videonya ke server. "
82
- "Mengembalikan download_url yang siap pakai selama 6 jam."
83
- ),
84
- "parameters": {
85
- "url": {
86
- "type": "string",
87
- "required": True,
88
- "description": "URL video TikTok."
89
- }
90
- },
91
- "example": "/api/tiktok?url=https://www.tiktok.com/@user/video/1234567890"
92
- },
93
- "/api/file/<file_id>": {
94
- "method": "GET",
95
- "description": "Akses file video yang sudah didownload. Berlaku selama 6 jam.",
96
- "parameters": {
97
- "file_id": {
98
- "type": "string",
99
- "required": True,
100
- "description": "ID file dari response /api/tiktok."
101
- }
102
- },
103
- "example": "/api/file/uuid-xxxxx"
104
- }
105
- }
106
- }
107
-
108
-
109
- def validate_tiktok_url(url):
110
- """Validasi apakah URL adalah URL TikTok yang valid."""
111
- patterns = [
112
- r'https?://(www\.)?tiktok\.com/@[\w.]+/video/\d+/?',
113
- r'https?://(vm|vt)\.tiktok\.com/[\w]+/?',
114
- r'https?://(www\.)?tiktok\.com/t/[\w]+/?',
115
- ]
116
- return any(re.match(pattern, url) for pattern in patterns)
117
-
118
-
119
- def extract_media_info(url, platform="tiktok"):
120
- """
121
- Menggunakan yt-dlp --dump-json untuk mengekstrak metadata video.
122
- """
123
- try:
124
- cmd = [
125
- "yt-dlp",
126
- "--dump-json",
127
- "--no-warnings",
128
- "--no-playlist",
129
- "--skip-download",
130
- url
131
- ]
132
-
133
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
134
-
135
- if result.returncode != 0:
136
- error_msg = result.stderr.strip() if result.stderr else "Unknown error"
137
- return {"error": True, "message": f"yt-dlp error: {error_msg}"}
138
-
139
- data = json.loads(result.stdout)
140
-
141
- formats = data.get("formats", [])
142
- best_video = None
143
- best_audio = None
144
-
145
- for fmt in formats:
146
- if fmt.get("vcodec", "none") != "none" and fmt.get("acodec", "none") != "none":
147
- if best_video is None or (fmt.get("height", 0) or 0) > (best_video.get("height", 0) or 0):
148
- best_video = fmt
149
- elif fmt.get("acodec", "none") != "none" and fmt.get("vcodec", "none") == "none":
150
- if best_audio is None:
151
- best_audio = fmt
152
-
153
- return {
154
- "status": "success",
155
- "platform": platform,
156
- "data": {
157
- "id": data.get("id"),
158
- "title": data.get("title", "Tidak ada judul"),
159
- "description": data.get("description", ""),
160
- "duration": data.get("duration"),
161
- "uploader": data.get("uploader", data.get("creator", "Unknown")),
162
- "uploader_url": data.get("uploader_url", ""),
163
- "thumbnail": data.get("thumbnail", ""),
164
- "view_count": data.get("view_count"),
165
- "like_count": data.get("like_count"),
166
- "comment_count": data.get("comment_count"),
167
- "upload_date": data.get("upload_date"),
168
- "resolution": f'{best_video.get("width", "?")}x{best_video.get("height", "?")}' if best_video else None,
169
- }
170
- }
171
-
172
- except subprocess.TimeoutExpired:
173
- return {"error": True, "message": "Request timeout: yt-dlp membutuhkan waktu terlalu lama."}
174
- except json.JSONDecodeError:
175
- return {"error": True, "message": "Gagal memparse response dari yt-dlp."}
176
- except Exception as e:
177
- return {"error": True, "message": f"Internal error: {str(e)}"}
178
-
179
-
180
- def download_video(url):
181
- """
182
- Download video TikTok ke temp file.
183
- Return: (file_id, file_path, filename) atau raise Exception.
184
- """
185
- file_id = str(uuid.uuid4())
186
- output_path = os.path.join(TEMP_DIR, f"{file_id}.mp4")
187
-
188
- cmd = [
189
- "yt-dlp",
190
- "--no-warnings",
191
- "--no-playlist",
192
- "-f", "bestvideo+bestaudio/best",
193
- "--merge-output-format", "mp4",
194
- "-o", output_path,
195
- url
196
- ]
197
-
198
- result = subprocess.run(cmd, capture_output=True, timeout=180)
199
-
200
- if result.returncode != 0:
201
- error_msg = result.stderr.decode(errors="ignore").strip()
202
- raise Exception(f"yt-dlp error: {error_msg}")
203
-
204
- if not os.path.exists(output_path):
205
- raise Exception("File tidak berhasil dibuat oleh yt-dlp.")
206
-
207
- return file_id, output_path
208
-
209
-
210
- # ========================================
211
- # Routes
212
- # ========================================
213
 
214
  @app.route("/")
215
  def index():
 
 
1
  import os
2
  import re
 
3
  import time
4
+ from flask import Flask, request, send_file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ from config import EXPIRE_SECONDS, API_LIST
7
+ from utils import jsonify, validate_tiktok_url
8
+ from cache import video_path, save_meta, load_meta, cleanup_expired
9
+ from extractor import extract_media_info, download_video
10
 
11
+ app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  @app.route("/")
14
  def index():
cache.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ from config import TEMP_DIR
5
+
6
+ def meta_path(file_id):
7
+ return os.path.join(TEMP_DIR, f"{file_id}.json")
8
+
9
+ def video_path(file_id):
10
+ return os.path.join(TEMP_DIR, f"{file_id}.mp4")
11
+
12
+ def save_meta(file_id, filename, expires_at):
13
+ """Simpan metadata file ke disk."""
14
+ with open(meta_path(file_id), "w") as f:
15
+ json.dump({"filename": filename, "expires_at": expires_at}, f)
16
+
17
+ def load_meta(file_id):
18
+ """Baca metadata file dari disk. Return None jika tidak ada."""
19
+ p = meta_path(file_id)
20
+ if not os.path.exists(p):
21
+ return None
22
+ try:
23
+ with open(p) as f:
24
+ return json.load(f)
25
+ except Exception:
26
+ return None
27
+
28
+ def cleanup_expired():
29
+ """Hapus file temp (video + meta) yang sudah expired."""
30
+ now = time.time()
31
+ for fname in os.listdir(TEMP_DIR):
32
+ if not fname.endswith(".json"):
33
+ continue
34
+ fid = fname[:-5]
35
+ meta = load_meta(fid)
36
+ if meta and meta["expires_at"] < now:
37
+ try:
38
+ vp = video_path(fid)
39
+ if os.path.exists(vp):
40
+ os.remove(vp)
41
+ os.remove(meta_path(fid))
42
+ except Exception:
43
+ pass
config.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ TEMP_DIR = "/tmp/ytdlp_cache"
4
+ EXPIRE_SECONDS = 6 * 3600 # 6 jam
5
+ os.makedirs(TEMP_DIR, exist_ok=True)
6
+
7
+ API_LIST = {
8
+ "name": "yt-dlp API",
9
+ "version": "1.0.0",
10
+ "description": "API untuk mengunduh dan mengekstrak informasi media dari berbagai platform menggunakan yt-dlp.",
11
+ "endpoints": {
12
+ "/api/tiktok": {
13
+ "method": "GET",
14
+ "description": (
15
+ "Mengekstrak info video TikTok sekaligus mendownload videonya ke server. "
16
+ "Mengembalikan download_url yang siap pakai selama 6 jam."
17
+ ),
18
+ "parameters": {
19
+ "url": {
20
+ "type": "string",
21
+ "required": True,
22
+ "description": "URL video TikTok."
23
+ }
24
+ },
25
+ "example": "/api/tiktok?url=https://www.tiktok.com/@user/video/1234567890"
26
+ },
27
+ "/api/file/<file_id>": {
28
+ "method": "GET",
29
+ "description": "Akses file video yang sudah didownload. Berlaku selama 6 jam.",
30
+ "parameters": {
31
+ "file_id": {
32
+ "type": "string",
33
+ "required": True,
34
+ "description": "ID file dari response /api/tiktok."
35
+ }
36
+ },
37
+ "example": "/api/file/uuid-xxxxx"
38
+ }
39
+ }
40
+ }
extractor.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import uuid
5
+ from config import TEMP_DIR
6
+
7
+ def extract_media_info(url, platform="tiktok"):
8
+ """
9
+ Menggunakan yt-dlp --dump-json untuk mengekstrak metadata video.
10
+ """
11
+ try:
12
+ cmd = [
13
+ "yt-dlp",
14
+ "--dump-json",
15
+ "--no-warnings",
16
+ "--no-playlist",
17
+ "--skip-download",
18
+ url
19
+ ]
20
+
21
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
22
+
23
+ if result.returncode != 0:
24
+ error_msg = result.stderr.strip() if result.stderr else "Unknown error"
25
+ return {"error": True, "message": f"yt-dlp error: {error_msg}"}
26
+
27
+ data = json.loads(result.stdout)
28
+
29
+ formats = data.get("formats", [])
30
+ best_video = None
31
+ best_audio = None
32
+
33
+ for fmt in formats:
34
+ if fmt.get("vcodec", "none") != "none" and fmt.get("acodec", "none") != "none":
35
+ if best_video is None or (fmt.get("height", 0) or 0) > (best_video.get("height", 0) or 0):
36
+ best_video = fmt
37
+ elif fmt.get("acodec", "none") != "none" and fmt.get("vcodec", "none") == "none":
38
+ if best_audio is None:
39
+ best_audio = fmt
40
+
41
+ return {
42
+ "status": "success",
43
+ "platform": platform,
44
+ "data": {
45
+ "id": data.get("id"),
46
+ "title": data.get("title", "Tidak ada judul"),
47
+ "description": data.get("description", ""),
48
+ "duration": data.get("duration"),
49
+ "uploader": data.get("uploader", data.get("creator", "Unknown")),
50
+ "uploader_url": data.get("uploader_url", ""),
51
+ "thumbnail": data.get("thumbnail", ""),
52
+ "view_count": data.get("view_count"),
53
+ "like_count": data.get("like_count"),
54
+ "comment_count": data.get("comment_count"),
55
+ "upload_date": data.get("upload_date"),
56
+ "resolution": f'{best_video.get("width", "?")}x{best_video.get("height", "?")}' if best_video else None,
57
+ }
58
+ }
59
+
60
+ except subprocess.TimeoutExpired:
61
+ return {"error": True, "message": "Request timeout: yt-dlp membutuhkan waktu terlalu lama."}
62
+ except json.JSONDecodeError:
63
+ return {"error": True, "message": "Gagal memparse response dari yt-dlp."}
64
+ except Exception as e:
65
+ return {"error": True, "message": f"Internal error: {str(e)}"}
66
+
67
+ def download_video(url):
68
+ """
69
+ Download video TikTok ke temp file.
70
+ Return: (file_id, output_path) atau raise Exception.
71
+ """
72
+ file_id = str(uuid.uuid4())
73
+ output_path = os.path.join(TEMP_DIR, f"{file_id}.mp4")
74
+
75
+ cmd = [
76
+ "yt-dlp",
77
+ "--no-warnings",
78
+ "--no-playlist",
79
+ "-f", "bestvideo+bestaudio/best",
80
+ "--merge-output-format", "mp4",
81
+ "-o", output_path,
82
+ url
83
+ ]
84
+
85
+ result = subprocess.run(cmd, capture_output=True, timeout=180)
86
+
87
+ if result.returncode != 0:
88
+ error_msg = result.stderr.decode(errors="ignore").strip()
89
+ raise Exception(f"yt-dlp error: {error_msg}")
90
+
91
+ if not os.path.exists(output_path):
92
+ raise Exception("File tidak berhasil dibuat oleh yt-dlp.")
93
+
94
+ return file_id, output_path
utils.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from flask import make_response
4
+
5
+ def jsonify(data, status=200):
6
+ """Custom jsonify dengan indent 2 dan ensure_ascii False."""
7
+ response = make_response(json.dumps(data, indent=2, ensure_ascii=False), status)
8
+ response.headers["Content-Type"] = "application/json; charset=utf-8"
9
+ return response
10
+
11
+ def validate_tiktok_url(url):
12
+ """Validasi apakah URL adalah URL TikTok yang valid."""
13
+ patterns = [
14
+ r'https?://(www\.)?tiktok\.com/@[\w.]+/video/\d+/?',
15
+ r'https?://(vm|vt)\.tiktok\.com/[\w]+/?',
16
+ r'https?://(www\.)?tiktok\.com/t/[\w]+/?',
17
+ ]
18
+ return any(re.match(pattern, url) for pattern in patterns)