Fix: Support TikTok photo posts by resolving redirects and normalizing /photo/ to /video/
Browse files
app.py
CHANGED
|
@@ -4,7 +4,7 @@ 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 |
|
|
@@ -23,6 +23,7 @@ def tiktok():
|
|
| 23 |
cleanup_expired()
|
| 24 |
|
| 25 |
url = request.args.get("url")
|
|
|
|
| 26 |
|
| 27 |
if not url:
|
| 28 |
return jsonify({
|
|
@@ -31,6 +32,13 @@ def tiktok():
|
|
| 31 |
"example": "/api/tiktok?url=https://www.tiktok.com/@user/video/1234567890"
|
| 32 |
}), 400
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
if not validate_tiktok_url(url):
|
| 35 |
return jsonify({
|
| 36 |
"status": "error",
|
|
@@ -43,19 +51,23 @@ def tiktok():
|
|
| 43 |
]
|
| 44 |
}), 400
|
| 45 |
|
| 46 |
-
#
|
| 47 |
info = extract_media_info(url, platform="tiktok")
|
| 48 |
if info.get("error"):
|
| 49 |
return jsonify({"status": "error", "message": info["message"]}), 500
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
try:
|
| 53 |
file_id, file_path = download_video(url)
|
| 54 |
except Exception as e:
|
| 55 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 56 |
|
| 57 |
-
raw = info["raw_data"]
|
| 58 |
-
|
| 59 |
# Deteksi author & nickname
|
| 60 |
author = raw.get("uploader_id")
|
| 61 |
if not author:
|
|
@@ -83,9 +95,14 @@ def tiktok():
|
|
| 83 |
audio_url = fmt.get("url")
|
| 84 |
break
|
| 85 |
|
| 86 |
-
# Deteksi slideshow / photo
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
# Konversi durasi ke format M:SS atau H:MM:SS
|
| 91 |
duration_secs = raw.get("duration")
|
|
@@ -104,14 +121,14 @@ def tiktok():
|
|
| 104 |
except Exception:
|
| 105 |
duration_str = str(duration_secs)
|
| 106 |
|
| 107 |
-
#
|
| 108 |
title = raw.get("title") or raw.get("description") or "video"
|
| 109 |
safe_title = re.sub(r'[^\w\s-]', '', title).strip()[:60] or "video"
|
| 110 |
filename = f"{safe_title}.mp4"
|
| 111 |
expires_at = time.time() + EXPIRE_SECONDS
|
| 112 |
save_meta(file_id, filename, expires_at)
|
| 113 |
|
| 114 |
-
#
|
| 115 |
base_url = request.host_url.rstrip("/")
|
| 116 |
video_url = f"{base_url}/api/file/{file_id}"
|
| 117 |
|
|
|
|
| 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, resolve_url
|
| 8 |
from cache import video_path, save_meta, load_meta, cleanup_expired
|
| 9 |
from extractor import extract_media_info, download_video
|
| 10 |
|
|
|
|
| 23 |
cleanup_expired()
|
| 24 |
|
| 25 |
url = request.args.get("url")
|
| 26 |
+
debug_mode = request.args.get("debug")
|
| 27 |
|
| 28 |
if not url:
|
| 29 |
return jsonify({
|
|
|
|
| 32 |
"example": "/api/tiktok?url=https://www.tiktok.com/@user/video/1234567890"
|
| 33 |
}), 400
|
| 34 |
|
| 35 |
+
# 1. Resolve redirect URL pendek (seperti vt.tiktok.com)
|
| 36 |
+
url = resolve_url(url)
|
| 37 |
+
|
| 38 |
+
# 2. Normalisasi URL photo ke video agar didukung oleh yt-dlp
|
| 39 |
+
if "/photo/" in url:
|
| 40 |
+
url = url.replace("/photo/", "/video/")
|
| 41 |
+
|
| 42 |
if not validate_tiktok_url(url):
|
| 43 |
return jsonify({
|
| 44 |
"status": "error",
|
|
|
|
| 51 |
]
|
| 52 |
}), 400
|
| 53 |
|
| 54 |
+
# 3. Ambil metadata
|
| 55 |
info = extract_media_info(url, platform="tiktok")
|
| 56 |
if info.get("error"):
|
| 57 |
return jsonify({"status": "error", "message": info["message"]}), 500
|
| 58 |
|
| 59 |
+
raw = info["raw_data"]
|
| 60 |
+
|
| 61 |
+
# Endpoint debug untuk melihat raw data yt-dlp langsung
|
| 62 |
+
if debug_mode in ["1", "true"]:
|
| 63 |
+
return jsonify(raw), 200
|
| 64 |
+
|
| 65 |
+
# 4. Download video/slideshow mp4 ke temp file
|
| 66 |
try:
|
| 67 |
file_id, file_path = download_video(url)
|
| 68 |
except Exception as e:
|
| 69 |
return jsonify({"status": "error", "message": str(e)}), 500
|
| 70 |
|
|
|
|
|
|
|
| 71 |
# Deteksi author & nickname
|
| 72 |
author = raw.get("uploader_id")
|
| 73 |
if not author:
|
|
|
|
| 95 |
audio_url = fmt.get("url")
|
| 96 |
break
|
| 97 |
|
| 98 |
+
# Deteksi slideshow / photo dari thumbnails
|
| 99 |
+
photo = False
|
| 100 |
+
thumbnails = raw.get("thumbnails", [])
|
| 101 |
+
if thumbnails:
|
| 102 |
+
photo_urls = [t.get("url") for t in thumbnails if t.get("url")]
|
| 103 |
+
# Jika ada lebih dari 1 thumbnail, biasanya itu adalah slide gambarnya
|
| 104 |
+
if len(photo_urls) > 1:
|
| 105 |
+
photo = photo_urls
|
| 106 |
|
| 107 |
# Konversi durasi ke format M:SS atau H:MM:SS
|
| 108 |
duration_secs = raw.get("duration")
|
|
|
|
| 121 |
except Exception:
|
| 122 |
duration_str = str(duration_secs)
|
| 123 |
|
| 124 |
+
# 5. Simpan metadata ke disk (filesystem — aman lintas worker)
|
| 125 |
title = raw.get("title") or raw.get("description") or "video"
|
| 126 |
safe_title = re.sub(r'[^\w\s-]', '', title).strip()[:60] or "video"
|
| 127 |
filename = f"{safe_title}.mp4"
|
| 128 |
expires_at = time.time() + EXPIRE_SECONDS
|
| 129 |
save_meta(file_id, filename, expires_at)
|
| 130 |
|
| 131 |
+
# 6. Susun response
|
| 132 |
base_url = request.host_url.rstrip("/")
|
| 133 |
video_url = f"{base_url}/api/file/{file_id}"
|
| 134 |
|
utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import json
|
| 2 |
import re
|
|
|
|
| 3 |
from flask import make_response
|
| 4 |
|
| 5 |
def jsonify(data, status=200):
|
|
@@ -9,10 +10,22 @@ def jsonify(data, status=200):
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import json
|
| 2 |
import re
|
| 3 |
+
import urllib.request
|
| 4 |
from flask import make_response
|
| 5 |
|
| 6 |
def jsonify(data, status=200):
|
|
|
|
| 10 |
return response
|
| 11 |
|
| 12 |
def validate_tiktok_url(url):
|
| 13 |
+
"""Validasi apakah URL adalah URL TikTok yang valid (video atau photo)."""
|
| 14 |
patterns = [
|
| 15 |
+
r'https?://(www\.)?tiktok\.com/@[\w.]+/(video|photo)/\d+/?',
|
| 16 |
r'https?://(vm|vt)\.tiktok\.com/[\w]+/?',
|
| 17 |
r'https?://(www\.)?tiktok\.com/t/[\w]+/?',
|
| 18 |
]
|
| 19 |
return any(re.match(pattern, url) for pattern in patterns)
|
| 20 |
+
|
| 21 |
+
def resolve_url(url):
|
| 22 |
+
"""Mengikuti redirect URL untuk mendapatkan URL final (misal dari vt.tiktok ke tiktok.com/@user/video/id)."""
|
| 23 |
+
try:
|
| 24 |
+
req = urllib.request.Request(
|
| 25 |
+
url,
|
| 26 |
+
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
|
| 27 |
+
)
|
| 28 |
+
with urllib.request.urlopen(req, timeout=15) as response:
|
| 29 |
+
return response.geturl()
|
| 30 |
+
except Exception:
|
| 31 |
+
return url
|