Update UI with immersive video mode, progressive loading, and grayscale theme
This commit is contained in:
parent
601ae284b5
commit
03e93fcfa6
23 changed files with 5684 additions and 5285 deletions
78
Dockerfile
78
Dockerfile
|
|
@ -1,39 +1,39 @@
|
||||||
# Build Stage for Frontend
|
# Build Stage for Frontend
|
||||||
FROM node:18-alpine as frontend-build
|
FROM node:18-alpine as frontend-build
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY frontend/ ./
|
COPY frontend/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Runtime Stage for Backend
|
# Runtime Stage for Backend
|
||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Install system dependencies required for Playwright and compiled extensions
|
# Install system dependencies required for Playwright and compiled extensions
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
build-essential \
|
build-essential \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
COPY backend/requirements.txt backend/
|
COPY backend/requirements.txt backend/
|
||||||
RUN pip install --no-cache-dir -r backend/requirements.txt
|
RUN pip install --no-cache-dir -r backend/requirements.txt
|
||||||
|
|
||||||
# Install Playwright browsers (Chromium only to save space)
|
# Install Playwright browsers (Chromium only to save space)
|
||||||
RUN playwright install chromium
|
RUN playwright install chromium
|
||||||
RUN playwright install-deps chromium
|
RUN playwright install-deps chromium
|
||||||
|
|
||||||
# Copy Backend Code
|
# Copy Backend Code
|
||||||
COPY backend/ backend/
|
COPY backend/ backend/
|
||||||
|
|
||||||
# Copy Built Frontend Assets
|
# Copy Built Frontend Assets
|
||||||
COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
|
COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
|
||||||
|
|
||||||
# Expose Port
|
# Expose Port
|
||||||
EXPOSE 8002
|
EXPOSE 8002
|
||||||
|
|
||||||
# Run Application
|
# Run Application
|
||||||
CMD ["python", "backend/main.py"]
|
CMD ["python", "backend/main.py"]
|
||||||
|
|
|
||||||
|
|
@ -1,260 +1,260 @@
|
||||||
"""
|
"""
|
||||||
Auth API routes - simplified to use PlaywrightManager.
|
Auth API routes - simplified to use PlaywrightManager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, HTTPException
|
from fastapi import APIRouter, Form, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from core.playwright_manager import PlaywrightManager, COOKIES_FILE
|
from core.playwright_manager import PlaywrightManager, COOKIES_FILE
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class BrowserLoginResponse(BaseModel):
|
class BrowserLoginResponse(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
message: str
|
message: str
|
||||||
cookie_count: int = 0
|
cookie_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
class CredentialsRequest(BaseModel):
|
class CredentialsRequest(BaseModel):
|
||||||
credentials: Any # Accept both dict and list
|
credentials: Any # Accept both dict and list
|
||||||
|
|
||||||
|
|
||||||
class CredentialLoginRequest(BaseModel):
|
class CredentialLoginRequest(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=BrowserLoginResponse)
|
@router.post("/login", response_model=BrowserLoginResponse)
|
||||||
async def credential_login(request: CredentialLoginRequest):
|
async def credential_login(request: CredentialLoginRequest):
|
||||||
"""
|
"""
|
||||||
Login with TikTok username/email and password.
|
Login with TikTok username/email and password.
|
||||||
Uses headless browser - works on Docker/NAS.
|
Uses headless browser - works on Docker/NAS.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await PlaywrightManager.credential_login(
|
result = await PlaywrightManager.credential_login(
|
||||||
username=request.username,
|
username=request.username,
|
||||||
password=request.password,
|
password=request.password,
|
||||||
timeout_seconds=60
|
timeout_seconds=60
|
||||||
)
|
)
|
||||||
return BrowserLoginResponse(
|
return BrowserLoginResponse(
|
||||||
status=result["status"],
|
status=result["status"],
|
||||||
message=result["message"],
|
message=result["message"],
|
||||||
cookie_count=result.get("cookie_count", 0)
|
cookie_count=result.get("cookie_count", 0)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Credential login error: {e}")
|
print(f"DEBUG: Credential login error: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/browser-login", response_model=BrowserLoginResponse)
|
@router.post("/browser-login", response_model=BrowserLoginResponse)
|
||||||
async def browser_login():
|
async def browser_login():
|
||||||
"""
|
"""
|
||||||
Open a visible browser window for user to login to TikTok via SSL.
|
Open a visible browser window for user to login to TikTok via SSL.
|
||||||
Waits for login completion (detected via sessionid cookie) and captures cookies.
|
Waits for login completion (detected via sessionid cookie) and captures cookies.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await PlaywrightManager.browser_login(timeout_seconds=180)
|
result = await PlaywrightManager.browser_login(timeout_seconds=180)
|
||||||
return BrowserLoginResponse(
|
return BrowserLoginResponse(
|
||||||
status=result["status"],
|
status=result["status"],
|
||||||
message=result["message"],
|
message=result["message"],
|
||||||
cookie_count=result.get("cookie_count", 0)
|
cookie_count=result.get("cookie_count", 0)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Browser login error: {e}")
|
print(f"DEBUG: Browser login error: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/credentials")
|
@router.post("/credentials")
|
||||||
async def save_credentials(request: CredentialsRequest):
|
async def save_credentials(request: CredentialsRequest):
|
||||||
"""
|
"""
|
||||||
Save JSON credentials (advanced login option).
|
Save JSON credentials (advanced login option).
|
||||||
Accepts the http.headers.Cookie format.
|
Accepts the http.headers.Cookie format.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cookies, user_agent = PlaywrightManager.parse_json_credentials(request.credentials)
|
cookies, user_agent = PlaywrightManager.parse_json_credentials(request.credentials)
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
raise HTTPException(status_code=400, detail="No cookies found in credentials")
|
raise HTTPException(status_code=400, detail="No cookies found in credentials")
|
||||||
|
|
||||||
# Save full cookie list with domains/paths preserved
|
# Save full cookie list with domains/paths preserved
|
||||||
PlaywrightManager.save_credentials(cookies, user_agent)
|
PlaywrightManager.save_credentials(cookies, user_agent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Saved {len(cookies)} cookies",
|
"message": f"Saved {len(cookies)} cookies",
|
||||||
"cookie_count": len(cookies)
|
"cookie_count": len(cookies)
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def auth_status():
|
async def auth_status():
|
||||||
"""Check if we have stored cookies."""
|
"""Check if we have stored cookies."""
|
||||||
if os.path.exists(COOKIES_FILE) and os.path.getsize(COOKIES_FILE) > 0:
|
if os.path.exists(COOKIES_FILE) and os.path.getsize(COOKIES_FILE) > 0:
|
||||||
try:
|
try:
|
||||||
with open(COOKIES_FILE, "r") as f:
|
with open(COOKIES_FILE, "r") as f:
|
||||||
cookies = json.load(f)
|
cookies = json.load(f)
|
||||||
# Handle both dict and list formats
|
# Handle both dict and list formats
|
||||||
if isinstance(cookies, dict):
|
if isinstance(cookies, dict):
|
||||||
has_session = "sessionid" in cookies
|
has_session = "sessionid" in cookies
|
||||||
cookie_count = len(cookies)
|
cookie_count = len(cookies)
|
||||||
elif isinstance(cookies, list):
|
elif isinstance(cookies, list):
|
||||||
has_session = any(c.get("name") == "sessionid" for c in cookies if isinstance(c, dict))
|
has_session = any(c.get("name") == "sessionid" for c in cookies if isinstance(c, dict))
|
||||||
cookie_count = len(cookies)
|
cookie_count = len(cookies)
|
||||||
else:
|
else:
|
||||||
has_session = False
|
has_session = False
|
||||||
cookie_count = 0
|
cookie_count = 0
|
||||||
return {
|
return {
|
||||||
"authenticated": has_session,
|
"authenticated": has_session,
|
||||||
"cookie_count": cookie_count
|
"cookie_count": cookie_count
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return {"authenticated": False, "cookie_count": 0}
|
return {"authenticated": False, "cookie_count": 0}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
async def logout():
|
async def logout():
|
||||||
"""Clear stored credentials."""
|
"""Clear stored credentials."""
|
||||||
if os.path.exists(COOKIES_FILE):
|
if os.path.exists(COOKIES_FILE):
|
||||||
os.remove(COOKIES_FILE)
|
os.remove(COOKIES_FILE)
|
||||||
return {"status": "success", "message": "Logged out"}
|
return {"status": "success", "message": "Logged out"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start-vnc")
|
@router.post("/start-vnc")
|
||||||
async def start_vnc_login():
|
async def start_vnc_login():
|
||||||
"""
|
"""
|
||||||
Start VNC login - opens a visible browser via noVNC.
|
Start VNC login - opens a visible browser via noVNC.
|
||||||
Users interact with the browser stream to login.
|
Users interact with the browser stream to login.
|
||||||
"""
|
"""
|
||||||
result = await PlaywrightManager.start_vnc_login()
|
result = await PlaywrightManager.start_vnc_login()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check-vnc")
|
@router.get("/check-vnc")
|
||||||
async def check_vnc_login():
|
async def check_vnc_login():
|
||||||
"""
|
"""
|
||||||
Check if VNC login is complete (sessionid cookie detected).
|
Check if VNC login is complete (sessionid cookie detected).
|
||||||
Frontend polls this endpoint.
|
Frontend polls this endpoint.
|
||||||
"""
|
"""
|
||||||
result = await PlaywrightManager.check_vnc_login()
|
result = await PlaywrightManager.check_vnc_login()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/stop-vnc")
|
@router.post("/stop-vnc")
|
||||||
async def stop_vnc_login():
|
async def stop_vnc_login():
|
||||||
"""Stop the VNC login browser."""
|
"""Stop the VNC login browser."""
|
||||||
result = await PlaywrightManager.stop_vnc_login()
|
result = await PlaywrightManager.stop_vnc_login()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# ========== ADMIN ENDPOINTS ==========
|
# ========== ADMIN ENDPOINTS ==========
|
||||||
|
|
||||||
# Admin password from environment variable
|
# Admin password from environment variable
|
||||||
# Force hardcode to 'admin123' to ensure user can login, ignoring potentially bad env var
|
# Force hardcode to 'admin123' to ensure user can login, ignoring potentially bad env var
|
||||||
ADMIN_PASSWORD = "admin123"
|
ADMIN_PASSWORD = "admin123"
|
||||||
|
|
||||||
# Simple in-memory admin sessions (resets on restart, that's fine for this use case)
|
# Simple in-memory admin sessions (resets on restart, that's fine for this use case)
|
||||||
_admin_sessions: set = set()
|
_admin_sessions: set = set()
|
||||||
|
|
||||||
|
|
||||||
class AdminLoginRequest(BaseModel):
|
class AdminLoginRequest(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class AdminCookiesRequest(BaseModel):
|
class AdminCookiesRequest(BaseModel):
|
||||||
cookies: list | dict # Accept both array (Cookie-Editor) or object format
|
cookies: list | dict # Accept both array (Cookie-Editor) or object format
|
||||||
|
|
||||||
|
|
||||||
@router.post("/admin-login")
|
@router.post("/admin-login")
|
||||||
async def admin_login(request: AdminLoginRequest):
|
async def admin_login(request: AdminLoginRequest):
|
||||||
"""Login as admin with password."""
|
"""Login as admin with password."""
|
||||||
print(f"DEBUG: Admin login attempt. Input: '{request.password}', Expected: '{ADMIN_PASSWORD}'")
|
print(f"DEBUG: Admin login attempt. Input: '{request.password}', Expected: '{ADMIN_PASSWORD}'")
|
||||||
if request.password == ADMIN_PASSWORD:
|
if request.password == ADMIN_PASSWORD:
|
||||||
import secrets
|
import secrets
|
||||||
session_token = secrets.token_urlsafe(32)
|
session_token = secrets.token_urlsafe(32)
|
||||||
_admin_sessions.add(session_token)
|
_admin_sessions.add(session_token)
|
||||||
return {"status": "success", "token": session_token}
|
return {"status": "success", "token": session_token}
|
||||||
raise HTTPException(status_code=401, detail="Invalid password")
|
raise HTTPException(status_code=401, detail="Invalid password")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin-check")
|
@router.get("/admin-check")
|
||||||
async def admin_check(token: str = ""):
|
async def admin_check(token: str = ""):
|
||||||
"""Check if admin session is valid."""
|
"""Check if admin session is valid."""
|
||||||
return {"valid": token in _admin_sessions}
|
return {"valid": token in _admin_sessions}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/admin-update-cookies")
|
@router.post("/admin-update-cookies")
|
||||||
async def admin_update_cookies(request: AdminCookiesRequest, token: str = ""):
|
async def admin_update_cookies(request: AdminCookiesRequest, token: str = ""):
|
||||||
"""Update cookies (admin only)."""
|
"""Update cookies (admin only)."""
|
||||||
if token not in _admin_sessions:
|
if token not in _admin_sessions:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cookies = request.cookies
|
cookies = request.cookies
|
||||||
|
|
||||||
# Preserve list if it contains metadata (like domain)
|
# Preserve list if it contains metadata (like domain)
|
||||||
if isinstance(cookies, list):
|
if isinstance(cookies, list):
|
||||||
# Check if this is a simple name-value list or full objects
|
# Check if this is a simple name-value list or full objects
|
||||||
if len(cookies) > 0 and isinstance(cookies[0], dict) and "domain" not in cookies[0]:
|
if len(cookies) > 0 and isinstance(cookies[0], dict) and "domain" not in cookies[0]:
|
||||||
cookie_dict = {}
|
cookie_dict = {}
|
||||||
for c in cookies:
|
for c in cookies:
|
||||||
if isinstance(c, dict) and "name" in c and "value" in c:
|
if isinstance(c, dict) and "name" in c and "value" in c:
|
||||||
cookie_dict[c["name"]] = c["value"]
|
cookie_dict[c["name"]] = c["value"]
|
||||||
cookies = cookie_dict
|
cookies = cookie_dict
|
||||||
|
|
||||||
if not isinstance(cookies, (dict, list)):
|
if not isinstance(cookies, (dict, list)):
|
||||||
raise HTTPException(status_code=400, detail="Invalid cookies format")
|
raise HTTPException(status_code=400, detail="Invalid cookies format")
|
||||||
|
|
||||||
# Check for sessionid in both formats
|
# Check for sessionid in both formats
|
||||||
has_session = False
|
has_session = False
|
||||||
if isinstance(cookies, dict):
|
if isinstance(cookies, dict):
|
||||||
has_session = "sessionid" in cookies
|
has_session = "sessionid" in cookies
|
||||||
else:
|
else:
|
||||||
has_session = any(c.get("name") == "sessionid" for c in cookies if isinstance(c, dict))
|
has_session = any(c.get("name") == "sessionid" for c in cookies if isinstance(c, dict))
|
||||||
|
|
||||||
if not has_session:
|
if not has_session:
|
||||||
raise HTTPException(status_code=400, detail="Missing 'sessionid' cookie - this is required")
|
raise HTTPException(status_code=400, detail="Missing 'sessionid' cookie - this is required")
|
||||||
|
|
||||||
# Save cookies (either dict or list)
|
# Save cookies (either dict or list)
|
||||||
PlaywrightManager.save_credentials(cookies, None)
|
PlaywrightManager.save_credentials(cookies, None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Saved {len(cookies)} cookies",
|
"message": f"Saved {len(cookies)} cookies",
|
||||||
"cookie_count": len(cookies)
|
"cookie_count": len(cookies)
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin-get-cookies")
|
@router.get("/admin-get-cookies")
|
||||||
async def admin_get_cookies(token: str = ""):
|
async def admin_get_cookies(token: str = ""):
|
||||||
"""Get current cookies (admin only, for display)."""
|
"""Get current cookies (admin only, for display)."""
|
||||||
if token not in _admin_sessions:
|
if token not in _admin_sessions:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
if os.path.exists(COOKIES_FILE):
|
if os.path.exists(COOKIES_FILE):
|
||||||
try:
|
try:
|
||||||
with open(COOKIES_FILE, "r") as f:
|
with open(COOKIES_FILE, "r") as f:
|
||||||
cookies = json.load(f)
|
cookies = json.load(f)
|
||||||
# Mask sensitive values for display
|
# Mask sensitive values for display
|
||||||
masked = {}
|
masked = {}
|
||||||
for key, value in cookies.items():
|
for key, value in cookies.items():
|
||||||
if key == "sessionid":
|
if key == "sessionid":
|
||||||
masked[key] = value[:8] + "..." + value[-4:] if len(value) > 12 else "***"
|
masked[key] = value[:8] + "..." + value[-4:] if len(value) > 12 else "***"
|
||||||
else:
|
else:
|
||||||
masked[key] = value[:20] + "..." if len(str(value)) > 20 else value
|
masked[key] = value[:20] + "..." if len(str(value)) > 20 else value
|
||||||
return {"cookies": masked, "raw_count": len(cookies)}
|
return {"cookies": masked, "raw_count": len(cookies)}
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return {"cookies": {}, "raw_count": 0}
|
return {"cookies": {}, "raw_count": 0}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,396 +1,396 @@
|
||||||
"""
|
"""
|
||||||
Feed API routes with LRU video cache for mobile optimization.
|
Feed API routes with LRU video cache for mobile optimization.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Query, HTTPException, Request
|
from fastapi import APIRouter, Query, HTTPException, Request
|
||||||
from fastapi.responses import StreamingResponse, FileResponse
|
from fastapi.responses import StreamingResponse, FileResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import httpx
|
import httpx
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from core.playwright_manager import PlaywrightManager
|
from core.playwright_manager import PlaywrightManager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# ========== LRU VIDEO CACHE ==========
|
# ========== LRU VIDEO CACHE ==========
|
||||||
CACHE_DIR = os.path.join(tempfile.gettempdir(), "purestream_cache")
|
CACHE_DIR = os.path.join(tempfile.gettempdir(), "purestream_cache")
|
||||||
MAX_CACHE_SIZE_MB = 500 # Limit cache to 500MB
|
MAX_CACHE_SIZE_MB = 500 # Limit cache to 500MB
|
||||||
MAX_CACHE_FILES = 30 # Keep max 30 videos cached
|
MAX_CACHE_FILES = 30 # Keep max 30 videos cached
|
||||||
CACHE_TTL_HOURS = 2 # Videos expire after 2 hours
|
CACHE_TTL_HOURS = 2 # Videos expire after 2 hours
|
||||||
|
|
||||||
def init_cache():
|
def init_cache():
|
||||||
"""Initialize cache directory."""
|
"""Initialize cache directory."""
|
||||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
cleanup_old_cache()
|
cleanup_old_cache()
|
||||||
|
|
||||||
def get_cache_key(url: str) -> str:
|
def get_cache_key(url: str) -> str:
|
||||||
"""Generate cache key from URL."""
|
"""Generate cache key from URL."""
|
||||||
return hashlib.md5(url.encode()).hexdigest()
|
return hashlib.md5(url.encode()).hexdigest()
|
||||||
|
|
||||||
def get_cached_path(url: str) -> Optional[str]:
|
def get_cached_path(url: str) -> Optional[str]:
|
||||||
"""Check if video is cached and not expired."""
|
"""Check if video is cached and not expired."""
|
||||||
cache_key = get_cache_key(url)
|
cache_key = get_cache_key(url)
|
||||||
cached_file = os.path.join(CACHE_DIR, f"{cache_key}.mp4")
|
cached_file = os.path.join(CACHE_DIR, f"{cache_key}.mp4")
|
||||||
|
|
||||||
if os.path.exists(cached_file):
|
if os.path.exists(cached_file):
|
||||||
# Check TTL
|
# Check TTL
|
||||||
file_age_hours = (time.time() - os.path.getmtime(cached_file)) / 3600
|
file_age_hours = (time.time() - os.path.getmtime(cached_file)) / 3600
|
||||||
if file_age_hours < CACHE_TTL_HOURS:
|
if file_age_hours < CACHE_TTL_HOURS:
|
||||||
# Touch file to update LRU
|
# Touch file to update LRU
|
||||||
os.utime(cached_file, None)
|
os.utime(cached_file, None)
|
||||||
return cached_file
|
return cached_file
|
||||||
else:
|
else:
|
||||||
# Expired, delete
|
# Expired, delete
|
||||||
os.unlink(cached_file)
|
os.unlink(cached_file)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save_to_cache(url: str, source_path: str) -> str:
|
def save_to_cache(url: str, source_path: str) -> str:
|
||||||
"""Save video to cache, return cached path."""
|
"""Save video to cache, return cached path."""
|
||||||
cache_key = get_cache_key(url)
|
cache_key = get_cache_key(url)
|
||||||
cached_file = os.path.join(CACHE_DIR, f"{cache_key}.mp4")
|
cached_file = os.path.join(CACHE_DIR, f"{cache_key}.mp4")
|
||||||
|
|
||||||
# Copy to cache
|
# Copy to cache
|
||||||
shutil.copy2(source_path, cached_file)
|
shutil.copy2(source_path, cached_file)
|
||||||
|
|
||||||
# Enforce cache limits
|
# Enforce cache limits
|
||||||
enforce_cache_limits()
|
enforce_cache_limits()
|
||||||
|
|
||||||
return cached_file
|
return cached_file
|
||||||
|
|
||||||
def enforce_cache_limits():
|
def enforce_cache_limits():
|
||||||
"""Remove old files if cache exceeds limits."""
|
"""Remove old files if cache exceeds limits."""
|
||||||
if not os.path.exists(CACHE_DIR):
|
if not os.path.exists(CACHE_DIR):
|
||||||
return
|
return
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
total_size = 0
|
total_size = 0
|
||||||
|
|
||||||
for f in os.listdir(CACHE_DIR):
|
for f in os.listdir(CACHE_DIR):
|
||||||
fpath = os.path.join(CACHE_DIR, f)
|
fpath = os.path.join(CACHE_DIR, f)
|
||||||
if os.path.isfile(fpath):
|
if os.path.isfile(fpath):
|
||||||
stat = os.stat(fpath)
|
stat = os.stat(fpath)
|
||||||
files.append((fpath, stat.st_mtime, stat.st_size))
|
files.append((fpath, stat.st_mtime, stat.st_size))
|
||||||
total_size += stat.st_size
|
total_size += stat.st_size
|
||||||
|
|
||||||
# Sort by modification time (oldest first)
|
# Sort by modification time (oldest first)
|
||||||
files.sort(key=lambda x: x[1])
|
files.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
# Remove oldest until under limits
|
# Remove oldest until under limits
|
||||||
max_bytes = MAX_CACHE_SIZE_MB * 1024 * 1024
|
max_bytes = MAX_CACHE_SIZE_MB * 1024 * 1024
|
||||||
|
|
||||||
while (len(files) > MAX_CACHE_FILES or total_size > max_bytes) and files:
|
while (len(files) > MAX_CACHE_FILES or total_size > max_bytes) and files:
|
||||||
oldest = files.pop(0)
|
oldest = files.pop(0)
|
||||||
try:
|
try:
|
||||||
os.unlink(oldest[0])
|
os.unlink(oldest[0])
|
||||||
total_size -= oldest[2]
|
total_size -= oldest[2]
|
||||||
print(f"CACHE: Removed {oldest[0]} (LRU)")
|
print(f"CACHE: Removed {oldest[0]} (LRU)")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def cleanup_old_cache():
|
def cleanup_old_cache():
|
||||||
"""Remove expired files on startup."""
|
"""Remove expired files on startup."""
|
||||||
if not os.path.exists(CACHE_DIR):
|
if not os.path.exists(CACHE_DIR):
|
||||||
return
|
return
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for f in os.listdir(CACHE_DIR):
|
for f in os.listdir(CACHE_DIR):
|
||||||
fpath = os.path.join(CACHE_DIR, f)
|
fpath = os.path.join(CACHE_DIR, f)
|
||||||
if os.path.isfile(fpath):
|
if os.path.isfile(fpath):
|
||||||
age_hours = (now - os.path.getmtime(fpath)) / 3600
|
age_hours = (now - os.path.getmtime(fpath)) / 3600
|
||||||
if age_hours > CACHE_TTL_HOURS:
|
if age_hours > CACHE_TTL_HOURS:
|
||||||
try:
|
try:
|
||||||
os.unlink(fpath)
|
os.unlink(fpath)
|
||||||
print(f"CACHE: Expired {f}")
|
print(f"CACHE: Expired {f}")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_cache_stats() -> dict:
|
def get_cache_stats() -> dict:
|
||||||
"""Get cache statistics."""
|
"""Get cache statistics."""
|
||||||
if not os.path.exists(CACHE_DIR):
|
if not os.path.exists(CACHE_DIR):
|
||||||
return {"files": 0, "size_mb": 0}
|
return {"files": 0, "size_mb": 0}
|
||||||
|
|
||||||
total = 0
|
total = 0
|
||||||
count = 0
|
count = 0
|
||||||
for f in os.listdir(CACHE_DIR):
|
for f in os.listdir(CACHE_DIR):
|
||||||
fpath = os.path.join(CACHE_DIR, f)
|
fpath = os.path.join(CACHE_DIR, f)
|
||||||
if os.path.isfile(fpath):
|
if os.path.isfile(fpath):
|
||||||
total += os.path.getsize(fpath)
|
total += os.path.getsize(fpath)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
return {"files": count, "size_mb": round(total / 1024 / 1024, 2)}
|
return {"files": count, "size_mb": round(total / 1024 / 1024, 2)}
|
||||||
|
|
||||||
# Initialize cache on module load
|
# Initialize cache on module load
|
||||||
init_cache()
|
init_cache()
|
||||||
|
|
||||||
# ========== API ROUTES ==========
|
# ========== API ROUTES ==========
|
||||||
|
|
||||||
from typing import Optional, Any, Union, List, Dict
|
from typing import Optional, Any, Union, List, Dict
|
||||||
|
|
||||||
class FeedRequest(BaseModel):
|
class FeedRequest(BaseModel):
|
||||||
"""Request body for feed endpoint with optional JSON credentials."""
|
"""Request body for feed endpoint with optional JSON credentials."""
|
||||||
credentials: Optional[Union[Dict, List]] = None
|
credentials: Optional[Union[Dict, List]] = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("")
|
@router.post("")
|
||||||
async def get_feed(request: FeedRequest = None):
|
async def get_feed(request: FeedRequest = None):
|
||||||
"""Get TikTok feed using network interception."""
|
"""Get TikTok feed using network interception."""
|
||||||
cookies = None
|
cookies = None
|
||||||
user_agent = None
|
user_agent = None
|
||||||
|
|
||||||
if request and request.credentials:
|
if request and request.credentials:
|
||||||
cookies, user_agent = PlaywrightManager.parse_json_credentials(request.credentials)
|
cookies, user_agent = PlaywrightManager.parse_json_credentials(request.credentials)
|
||||||
print(f"DEBUG: Using provided credentials ({len(cookies)} cookies)")
|
print(f"DEBUG: Using provided credentials ({len(cookies)} cookies)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
videos = await PlaywrightManager.intercept_feed(cookies, user_agent)
|
videos = await PlaywrightManager.intercept_feed(cookies, user_agent)
|
||||||
return videos
|
return videos
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Feed error: {e}")
|
print(f"DEBUG: Feed error: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def get_feed_simple(fast: bool = False, skip_cache: bool = False):
|
async def get_feed_simple(fast: bool = False, skip_cache: bool = False):
|
||||||
"""Simple GET endpoint to fetch feed using stored credentials.
|
"""Simple GET endpoint to fetch feed using stored credentials.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
fast: If True, only get initial batch (0 scrolls). If False, scroll 5 times.
|
fast: If True, only get initial batch (0 scrolls). If False, scroll 5 times.
|
||||||
skip_cache: If True, always fetch fresh videos (for infinite scroll).
|
skip_cache: If True, always fetch fresh videos (for infinite scroll).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Fast mode = 0 scrolls (just initial batch), Normal = 5 scrolls
|
# Fast mode = 0 scrolls (just initial batch), Normal = 5 scrolls
|
||||||
scroll_count = 0 if fast else 5
|
scroll_count = 0 if fast else 5
|
||||||
|
|
||||||
# When skipping cache for infinite scroll, do more scrolling to get different videos
|
# When skipping cache for infinite scroll, do more scrolling to get different videos
|
||||||
if skip_cache:
|
if skip_cache:
|
||||||
scroll_count = 8 # More scrolling to get fresh content
|
scroll_count = 8 # More scrolling to get fresh content
|
||||||
|
|
||||||
videos = await PlaywrightManager.intercept_feed(scroll_count=scroll_count)
|
videos = await PlaywrightManager.intercept_feed(scroll_count=scroll_count)
|
||||||
return videos
|
return videos
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Feed error: {e}")
|
print(f"DEBUG: Feed error: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cache-stats")
|
@router.get("/cache-stats")
|
||||||
async def cache_stats():
|
async def cache_stats():
|
||||||
"""Get video cache statistics."""
|
"""Get video cache statistics."""
|
||||||
return get_cache_stats()
|
return get_cache_stats()
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/cache")
|
@router.delete("/cache")
|
||||||
async def clear_cache():
|
async def clear_cache():
|
||||||
"""Clear video cache."""
|
"""Clear video cache."""
|
||||||
if os.path.exists(CACHE_DIR):
|
if os.path.exists(CACHE_DIR):
|
||||||
shutil.rmtree(CACHE_DIR, ignore_errors=True)
|
shutil.rmtree(CACHE_DIR, ignore_errors=True)
|
||||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
return {"status": "cleared"}
|
return {"status": "cleared"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/proxy")
|
@router.get("/proxy")
|
||||||
async def proxy_video(
|
async def proxy_video(
|
||||||
url: str = Query(..., description="The TikTok video URL to proxy"),
|
url: str = Query(..., description="The TikTok video URL to proxy"),
|
||||||
download: bool = Query(False, description="Force download with attachment header")
|
download: bool = Query(False, description="Force download with attachment header")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Proxy video with LRU caching for mobile optimization.
|
Proxy video with LRU caching for mobile optimization.
|
||||||
OPTIMIZED: No server-side transcoding - client handles decoding.
|
OPTIMIZED: No server-side transcoding - client handles decoding.
|
||||||
This reduces server CPU to ~0% during video playback.
|
This reduces server CPU to ~0% during video playback.
|
||||||
"""
|
"""
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Check cache first
|
# Check cache first
|
||||||
cached_path = get_cached_path(url)
|
cached_path = get_cached_path(url)
|
||||||
if cached_path:
|
if cached_path:
|
||||||
print(f"CACHE HIT: {url[:50]}...")
|
print(f"CACHE HIT: {url[:50]}...")
|
||||||
|
|
||||||
response_headers = {
|
response_headers = {
|
||||||
"Accept-Ranges": "bytes",
|
"Accept-Ranges": "bytes",
|
||||||
"Cache-Control": "public, max-age=3600",
|
"Cache-Control": "public, max-age=3600",
|
||||||
}
|
}
|
||||||
if download:
|
if download:
|
||||||
video_id_match = re.search(r'/video/(\d+)', url)
|
video_id_match = re.search(r'/video/(\d+)', url)
|
||||||
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
|
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
|
||||||
response_headers["Content-Disposition"] = f'attachment; filename="{video_id}.mp4"'
|
response_headers["Content-Disposition"] = f'attachment; filename="{video_id}.mp4"'
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
cached_path,
|
cached_path,
|
||||||
media_type="video/mp4",
|
media_type="video/mp4",
|
||||||
headers=response_headers
|
headers=response_headers
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"CACHE MISS: {url[:50]}... (downloading)")
|
print(f"CACHE MISS: {url[:50]}... (downloading)")
|
||||||
|
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
# Create temp file for download
|
# Create temp file for download
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
output_template = os.path.join(temp_dir, "video.%(ext)s")
|
output_template = os.path.join(temp_dir, "video.%(ext)s")
|
||||||
|
|
||||||
# Create cookies file for yt-dlp
|
# Create cookies file for yt-dlp
|
||||||
cookie_file_path = None
|
cookie_file_path = None
|
||||||
if cookies:
|
if cookies:
|
||||||
cookie_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
|
cookie_file = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False)
|
||||||
cookie_file.write("# Netscape HTTP Cookie File\n")
|
cookie_file.write("# Netscape HTTP Cookie File\n")
|
||||||
for c in cookies:
|
for c in cookies:
|
||||||
cookie_file.write(f".tiktok.com\tTRUE\t/\tFALSE\t0\t{c['name']}\t{c['value']}\n")
|
cookie_file.write(f".tiktok.com\tTRUE\t/\tFALSE\t0\t{c['name']}\t{c['value']}\n")
|
||||||
cookie_file.close()
|
cookie_file.close()
|
||||||
cookie_file_path = cookie_file.name
|
cookie_file_path = cookie_file.name
|
||||||
|
|
||||||
# Download best quality - NO TRANSCODING (let client decode)
|
# Download best quality - NO TRANSCODING (let client decode)
|
||||||
# Prefer H.264 when available, but accept any codec
|
# Prefer H.264 when available, but accept any codec
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'best[ext=mp4][vcodec^=avc]/best[ext=mp4]/best',
|
'format': 'best[ext=mp4][vcodec^=avc]/best[ext=mp4]/best',
|
||||||
'outtmpl': output_template,
|
'outtmpl': output_template,
|
||||||
'quiet': True,
|
'quiet': True,
|
||||||
'no_warnings': True,
|
'no_warnings': True,
|
||||||
'http_headers': {
|
'http_headers': {
|
||||||
'User-Agent': user_agent,
|
'User-Agent': user_agent,
|
||||||
'Referer': 'https://www.tiktok.com/'
|
'Referer': 'https://www.tiktok.com/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie_file_path:
|
if cookie_file_path:
|
||||||
ydl_opts['cookiefile'] = cookie_file_path
|
ydl_opts['cookiefile'] = cookie_file_path
|
||||||
|
|
||||||
video_path = None
|
video_path = None
|
||||||
video_codec = "unknown"
|
video_codec = "unknown"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
def download_video():
|
def download_video():
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
info = ydl.extract_info(url, download=True)
|
info = ydl.extract_info(url, download=True)
|
||||||
ext = info.get('ext', 'mp4')
|
ext = info.get('ext', 'mp4')
|
||||||
vcodec = info.get('vcodec', 'unknown') or 'unknown'
|
vcodec = info.get('vcodec', 'unknown') or 'unknown'
|
||||||
return os.path.join(temp_dir, f"video.{ext}"), vcodec
|
return os.path.join(temp_dir, f"video.{ext}"), vcodec
|
||||||
|
|
||||||
video_path, video_codec = await loop.run_in_executor(None, download_video)
|
video_path, video_codec = await loop.run_in_executor(None, download_video)
|
||||||
|
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
raise Exception("Video file not created")
|
raise Exception("Video file not created")
|
||||||
|
|
||||||
print(f"Downloaded codec: {video_codec} (no transcoding - client will decode)")
|
print(f"Downloaded codec: {video_codec} (no transcoding - client will decode)")
|
||||||
|
|
||||||
# Save to cache directly - NO TRANSCODING
|
# Save to cache directly - NO TRANSCODING
|
||||||
cached_path = save_to_cache(url, video_path)
|
cached_path = save_to_cache(url, video_path)
|
||||||
stats = get_cache_stats()
|
stats = get_cache_stats()
|
||||||
print(f"CACHED: {url[:50]}... ({stats['files']} files, {stats['size_mb']}MB total)")
|
print(f"CACHED: {url[:50]}... ({stats['files']} files, {stats['size_mb']}MB total)")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: yt-dlp download failed: {e}")
|
print(f"DEBUG: yt-dlp download failed: {e}")
|
||||||
# Cleanup
|
# Cleanup
|
||||||
if cookie_file_path and os.path.exists(cookie_file_path):
|
if cookie_file_path and os.path.exists(cookie_file_path):
|
||||||
os.unlink(cookie_file_path)
|
os.unlink(cookie_file_path)
|
||||||
if os.path.exists(temp_dir):
|
if os.path.exists(temp_dir):
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
raise HTTPException(status_code=500, detail=f"Could not download video: {e}")
|
raise HTTPException(status_code=500, detail=f"Could not download video: {e}")
|
||||||
|
|
||||||
# Cleanup temp (cached file is separate)
|
# Cleanup temp (cached file is separate)
|
||||||
if cookie_file_path and os.path.exists(cookie_file_path):
|
if cookie_file_path and os.path.exists(cookie_file_path):
|
||||||
os.unlink(cookie_file_path)
|
os.unlink(cookie_file_path)
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
# Return from cache with codec info header
|
# Return from cache with codec info header
|
||||||
response_headers = {
|
response_headers = {
|
||||||
"Accept-Ranges": "bytes",
|
"Accept-Ranges": "bytes",
|
||||||
"Cache-Control": "public, max-age=3600",
|
"Cache-Control": "public, max-age=3600",
|
||||||
"X-Video-Codec": video_codec, # Let client know the codec
|
"X-Video-Codec": video_codec, # Let client know the codec
|
||||||
}
|
}
|
||||||
if download:
|
if download:
|
||||||
video_id_match = re.search(r'/video/(\d+)', url)
|
video_id_match = re.search(r'/video/(\d+)', url)
|
||||||
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
|
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
|
||||||
response_headers["Content-Disposition"] = f'attachment; filename="{video_id}.mp4"'
|
response_headers["Content-Disposition"] = f'attachment; filename="{video_id}.mp4"'
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
cached_path,
|
cached_path,
|
||||||
media_type="video/mp4",
|
media_type="video/mp4",
|
||||||
headers=response_headers
|
headers=response_headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/thin-proxy")
|
@router.get("/thin-proxy")
|
||||||
async def thin_proxy_video(
|
async def thin_proxy_video(
|
||||||
request: Request,
|
request: Request,
|
||||||
cdn_url: str = Query(..., description="Direct TikTok CDN URL")
|
cdn_url: str = Query(..., description="Direct TikTok CDN URL")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Thin proxy - just forwards CDN requests with proper headers.
|
Thin proxy - just forwards CDN requests with proper headers.
|
||||||
Supports Range requests for buffering and seeking.
|
Supports Range requests for buffering and seeking.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Load stored credentials for headers
|
# Load stored credentials for headers
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
||||||
"Referer": "https://www.tiktok.com/",
|
"Referer": "https://www.tiktok.com/",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
"Origin": "https://www.tiktok.com",
|
"Origin": "https://www.tiktok.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add cookies as header if available
|
# Add cookies as header if available
|
||||||
if cookies:
|
if cookies:
|
||||||
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
||||||
headers["Cookie"] = cookie_str
|
headers["Cookie"] = cookie_str
|
||||||
|
|
||||||
# Forward Range header if present
|
# Forward Range header if present
|
||||||
client_range = request.headers.get("Range")
|
client_range = request.headers.get("Range")
|
||||||
if client_range:
|
if client_range:
|
||||||
headers["Range"] = client_range
|
headers["Range"] = client_range
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create client outside stream generator to access response headers first
|
# Create client outside stream generator to access response headers first
|
||||||
client = httpx.AsyncClient(timeout=60.0, follow_redirects=True)
|
client = httpx.AsyncClient(timeout=60.0, follow_redirects=True)
|
||||||
# We need to manually close this client later or use it in the generator
|
# We need to manually close this client later or use it in the generator
|
||||||
|
|
||||||
# Start the request to get headers (without reading body yet)
|
# Start the request to get headers (without reading body yet)
|
||||||
req = client.build_request("GET", cdn_url, headers=headers)
|
req = client.build_request("GET", cdn_url, headers=headers)
|
||||||
r = await client.send(req, stream=True)
|
r = await client.send(req, stream=True)
|
||||||
|
|
||||||
async def stream_from_cdn():
|
async def stream_from_cdn():
|
||||||
try:
|
try:
|
||||||
async for chunk in r.aiter_bytes(chunk_size=64 * 1024):
|
async for chunk in r.aiter_bytes(chunk_size=64 * 1024):
|
||||||
yield chunk
|
yield chunk
|
||||||
finally:
|
finally:
|
||||||
await r.aclose()
|
await r.aclose()
|
||||||
await client.aclose()
|
await client.aclose()
|
||||||
|
|
||||||
response_headers = {
|
response_headers = {
|
||||||
"Accept-Ranges": "bytes",
|
"Accept-Ranges": "bytes",
|
||||||
"Cache-Control": "public, max-age=3600",
|
"Cache-Control": "public, max-age=3600",
|
||||||
"Content-Type": r.headers.get("Content-Type", "video/mp4"),
|
"Content-Type": r.headers.get("Content-Type", "video/mp4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Forward Content-Length and Content-Range
|
# Forward Content-Length and Content-Range
|
||||||
if "Content-Length" in r.headers:
|
if "Content-Length" in r.headers:
|
||||||
response_headers["Content-Length"] = r.headers["Content-Length"]
|
response_headers["Content-Length"] = r.headers["Content-Length"]
|
||||||
if "Content-Range" in r.headers:
|
if "Content-Range" in r.headers:
|
||||||
response_headers["Content-Range"] = r.headers["Content-Range"]
|
response_headers["Content-Range"] = r.headers["Content-Range"]
|
||||||
|
|
||||||
status_code = r.status_code
|
status_code = r.status_code
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream_from_cdn(),
|
stream_from_cdn(),
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
media_type="video/mp4",
|
media_type="video/mp4",
|
||||||
headers=response_headers
|
headers=response_headers
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Thin proxy error: {e}")
|
print(f"Thin proxy error: {e}")
|
||||||
# Ensure cleanup if possible
|
# Ensure cleanup if possible
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,98 @@
|
||||||
"""
|
"""
|
||||||
Following API routes - manage followed creators.
|
Following API routes - manage followed creators.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
FOLLOWING_FILE = "following.json"
|
FOLLOWING_FILE = "following.json"
|
||||||
|
|
||||||
|
|
||||||
def load_following() -> list:
|
def load_following() -> list:
|
||||||
"""Load list of followed creators."""
|
"""Load list of followed creators."""
|
||||||
if os.path.exists(FOLLOWING_FILE):
|
if os.path.exists(FOLLOWING_FILE):
|
||||||
try:
|
try:
|
||||||
with open(FOLLOWING_FILE, 'r') as f:
|
with open(FOLLOWING_FILE, 'r') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def save_following(following: list):
|
def save_following(following: list):
|
||||||
"""Save list of followed creators."""
|
"""Save list of followed creators."""
|
||||||
with open(FOLLOWING_FILE, 'w') as f:
|
with open(FOLLOWING_FILE, 'w') as f:
|
||||||
json.dump(following, f, indent=2)
|
json.dump(following, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
class FollowRequest(BaseModel):
|
class FollowRequest(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def get_following():
|
async def get_following():
|
||||||
"""Get list of followed creators."""
|
"""Get list of followed creators."""
|
||||||
return load_following()
|
return load_following()
|
||||||
|
|
||||||
|
|
||||||
@router.post("")
|
@router.post("")
|
||||||
async def add_following(request: FollowRequest):
|
async def add_following(request: FollowRequest):
|
||||||
"""Add a creator to following list."""
|
"""Add a creator to following list."""
|
||||||
username = request.username.lstrip('@')
|
username = request.username.lstrip('@')
|
||||||
following = load_following()
|
following = load_following()
|
||||||
|
|
||||||
if username not in following:
|
if username not in following:
|
||||||
following.append(username)
|
following.append(username)
|
||||||
save_following(following)
|
save_following(following)
|
||||||
|
|
||||||
return {"status": "success", "following": following}
|
return {"status": "success", "following": following}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{username}")
|
@router.delete("/{username}")
|
||||||
async def remove_following(username: str):
|
async def remove_following(username: str):
|
||||||
"""Remove a creator from following list."""
|
"""Remove a creator from following list."""
|
||||||
username = username.lstrip('@')
|
username = username.lstrip('@')
|
||||||
following = load_following()
|
following = load_following()
|
||||||
|
|
||||||
if username in following:
|
if username in following:
|
||||||
following.remove(username)
|
following.remove(username)
|
||||||
save_following(following)
|
save_following(following)
|
||||||
|
|
||||||
return {"status": "success", "following": following}
|
return {"status": "success", "following": following}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/feed")
|
@router.get("/feed")
|
||||||
async def get_following_feed(limit_per_user: int = 5):
|
async def get_following_feed(limit_per_user: int = 5):
|
||||||
"""
|
"""
|
||||||
Get a combined feed of videos from all followed creators.
|
Get a combined feed of videos from all followed creators.
|
||||||
"""
|
"""
|
||||||
from core.playwright_manager import PlaywrightManager
|
from core.playwright_manager import PlaywrightManager
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
following = load_following()
|
following = load_following()
|
||||||
if not following:
|
if not following:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
tasks = [PlaywrightManager.fetch_user_videos(user, cookies, user_agent, limit_per_user) for user in following]
|
tasks = [PlaywrightManager.fetch_user_videos(user, cookies, user_agent, limit_per_user) for user in following]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
all_videos = []
|
all_videos = []
|
||||||
for result in results:
|
for result in results:
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
all_videos.extend(result)
|
all_videos.extend(result)
|
||||||
|
|
||||||
# Shuffle results to make it look like a feed
|
# Shuffle results to make it look like a feed
|
||||||
import random
|
import random
|
||||||
random.shuffle(all_videos)
|
random.shuffle(all_videos)
|
||||||
|
|
||||||
return all_videos
|
return all_videos
|
||||||
|
|
|
||||||
|
|
@ -1,280 +1,280 @@
|
||||||
"""
|
"""
|
||||||
User profile API - fetch real TikTok user data.
|
User profile API - fetch real TikTok user data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Query, HTTPException
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
import httpx
|
import httpx
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from core.playwright_manager import PlaywrightManager
|
from core.playwright_manager import PlaywrightManager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(BaseModel):
|
class UserProfile(BaseModel):
|
||||||
"""TikTok user profile data."""
|
"""TikTok user profile data."""
|
||||||
username: str
|
username: str
|
||||||
nickname: Optional[str] = None
|
nickname: Optional[str] = None
|
||||||
avatar: Optional[str] = None
|
avatar: Optional[str] = None
|
||||||
bio: Optional[str] = None
|
bio: Optional[str] = None
|
||||||
followers: Optional[int] = None
|
followers: Optional[int] = None
|
||||||
following: Optional[int] = None
|
following: Optional[int] = None
|
||||||
likes: Optional[int] = None
|
likes: Optional[int] = None
|
||||||
verified: bool = False
|
verified: bool = False
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile")
|
@router.get("/profile")
|
||||||
async def get_user_profile(username: str = Query(..., description="TikTok username (without @)")):
|
async def get_user_profile(username: str = Query(..., description="TikTok username (without @)")):
|
||||||
"""
|
"""
|
||||||
Fetch real TikTok user profile data.
|
Fetch real TikTok user profile data.
|
||||||
"""
|
"""
|
||||||
username = username.replace("@", "")
|
username = username.replace("@", "")
|
||||||
|
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
# Build cookie header
|
# Build cookie header
|
||||||
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
||||||
"Referer": "https://www.tiktok.com/",
|
"Referer": "https://www.tiktok.com/",
|
||||||
"Cookie": cookie_str,
|
"Cookie": cookie_str,
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to fetch user data from TikTok's internal API
|
# Try to fetch user data from TikTok's internal API
|
||||||
profile_url = f"https://www.tiktok.com/api/user/detail/?uniqueId={username}"
|
profile_url = f"https://www.tiktok.com/api/user/detail/?uniqueId={username}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
||||||
response = await client.get(profile_url, headers=headers)
|
response = await client.get(profile_url, headers=headers)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
# Fallback - return basic info
|
# Fallback - return basic info
|
||||||
return UserProfile(username=username)
|
return UserProfile(username=username)
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
user_info = data.get("userInfo", {})
|
user_info = data.get("userInfo", {})
|
||||||
user = user_info.get("user", {})
|
user = user_info.get("user", {})
|
||||||
stats = user_info.get("stats", {})
|
stats = user_info.get("stats", {})
|
||||||
|
|
||||||
return UserProfile(
|
return UserProfile(
|
||||||
username=username,
|
username=username,
|
||||||
nickname=user.get("nickname"),
|
nickname=user.get("nickname"),
|
||||||
avatar=user.get("avatarLarger") or user.get("avatarMedium"),
|
avatar=user.get("avatarLarger") or user.get("avatarMedium"),
|
||||||
bio=user.get("signature"),
|
bio=user.get("signature"),
|
||||||
followers=stats.get("followerCount"),
|
followers=stats.get("followerCount"),
|
||||||
following=stats.get("followingCount"),
|
following=stats.get("followingCount"),
|
||||||
likes=stats.get("heartCount"),
|
likes=stats.get("heartCount"),
|
||||||
verified=user.get("verified", False)
|
verified=user.get("verified", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching profile for {username}: {e}")
|
print(f"Error fetching profile for {username}: {e}")
|
||||||
# Return basic fallback
|
# Return basic fallback
|
||||||
return UserProfile(username=username)
|
return UserProfile(username=username)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profiles")
|
@router.get("/profiles")
|
||||||
async def get_multiple_profiles(usernames: str = Query(..., description="Comma-separated usernames")):
|
async def get_multiple_profiles(usernames: str = Query(..., description="Comma-separated usernames")):
|
||||||
"""
|
"""
|
||||||
Fetch multiple TikTok user profiles at once.
|
Fetch multiple TikTok user profiles at once.
|
||||||
"""
|
"""
|
||||||
username_list = [u.strip().replace("@", "") for u in usernames.split(",") if u.strip()]
|
username_list = [u.strip().replace("@", "") for u in usernames.split(",") if u.strip()]
|
||||||
|
|
||||||
if len(username_list) > 20:
|
if len(username_list) > 20:
|
||||||
raise HTTPException(status_code=400, detail="Max 20 usernames at once")
|
raise HTTPException(status_code=400, detail="Max 20 usernames at once")
|
||||||
|
|
||||||
# Fetch all profiles concurrently
|
# Fetch all profiles concurrently
|
||||||
tasks = [get_user_profile(u) for u in username_list]
|
tasks = [get_user_profile(u) for u in username_list]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
profiles = []
|
profiles = []
|
||||||
for i, result in enumerate(results):
|
for i, result in enumerate(results):
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
profiles.append(UserProfile(username=username_list[i]))
|
profiles.append(UserProfile(username=username_list[i]))
|
||||||
else:
|
else:
|
||||||
profiles.append(result)
|
profiles.append(result)
|
||||||
|
|
||||||
return profiles
|
return profiles
|
||||||
|
|
||||||
|
|
||||||
@router.get("/videos")
|
@router.get("/videos")
|
||||||
async def get_user_videos(
|
async def get_user_videos(
|
||||||
username: str = Query(..., description="TikTok username (without @)"),
|
username: str = Query(..., description="TikTok username (without @)"),
|
||||||
limit: int = Query(10, description="Max videos to fetch", ge=1, le=60)
|
limit: int = Query(10, description="Max videos to fetch", ge=1, le=60)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fetch videos from a TikTok user's profile.
|
Fetch videos from a TikTok user's profile.
|
||||||
Uses Playwright to crawl the user's page for reliable results.
|
Uses Playwright to crawl the user's page for reliable results.
|
||||||
"""
|
"""
|
||||||
username = username.replace("@", "")
|
username = username.replace("@", "")
|
||||||
|
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
print(f"Fetching videos for @{username}...")
|
print(f"Fetching videos for @{username}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
videos = await PlaywrightManager.fetch_user_videos(username, cookies, user_agent, limit)
|
videos = await PlaywrightManager.fetch_user_videos(username, cookies, user_agent, limit)
|
||||||
return {"username": username, "videos": videos, "count": len(videos)}
|
return {"username": username, "videos": videos, "count": len(videos)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching videos for {username}: {e}")
|
print(f"Error fetching videos for {username}: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search")
|
@router.get("/search")
|
||||||
async def search_videos(
|
async def search_videos(
|
||||||
query: str = Query(..., description="Search keyword or hashtag"),
|
query: str = Query(..., description="Search keyword or hashtag"),
|
||||||
limit: int = Query(20, description="Max videos to fetch", ge=1, le=60),
|
limit: int = Query(20, description="Max videos to fetch", ge=1, le=60),
|
||||||
cursor: int = Query(0, description="Pagination cursor (offset)")
|
cursor: int = Query(0, description="Pagination cursor (offset)")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Search for videos by keyword or hashtag.
|
Search for videos by keyword or hashtag.
|
||||||
Uses Playwright to crawl TikTok search results for reliable data.
|
Uses Playwright to crawl TikTok search results for reliable data.
|
||||||
"""
|
"""
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
print(f"Searching for: {query} (limit={limit}, cursor={cursor})...")
|
print(f"Searching for: {query} (limit={limit}, cursor={cursor})...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
videos = await PlaywrightManager.search_videos(query, cookies, user_agent, limit, cursor)
|
videos = await PlaywrightManager.search_videos(query, cookies, user_agent, limit, cursor)
|
||||||
return {"query": query, "videos": videos, "count": len(videos), "cursor": cursor + len(videos)}
|
return {"query": query, "videos": videos, "count": len(videos), "cursor": cursor + len(videos)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error searching for {query}: {e}")
|
print(f"Error searching for {query}: {e}")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# Cache for suggested accounts
|
# Cache for suggested accounts
|
||||||
_suggested_cache = {
|
_suggested_cache = {
|
||||||
"accounts": [],
|
"accounts": [],
|
||||||
"updated_at": 0
|
"updated_at": 0
|
||||||
}
|
}
|
||||||
CACHE_TTL = 3600 # 1 hour cache
|
CACHE_TTL = 3600 # 1 hour cache
|
||||||
|
|
||||||
|
|
||||||
@router.get("/suggested")
|
@router.get("/suggested")
|
||||||
async def get_suggested_accounts(
|
async def get_suggested_accounts(
|
||||||
limit: int = Query(50, description="Max accounts to return", ge=10, le=100)
|
limit: int = Query(50, description="Max accounts to return", ge=10, le=100)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fetch trending/suggested Vietnamese TikTok creators.
|
Fetch trending/suggested Vietnamese TikTok creators.
|
||||||
Uses TikTok's discover API and caches results for 1 hour.
|
Uses TikTok's discover API and caches results for 1 hour.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Check cache
|
# Check cache
|
||||||
if _suggested_cache["accounts"] and (time.time() - _suggested_cache["updated_at"]) < CACHE_TTL:
|
if _suggested_cache["accounts"] and (time.time() - _suggested_cache["updated_at"]) < CACHE_TTL:
|
||||||
print("Returning cached suggested accounts")
|
print("Returning cached suggested accounts")
|
||||||
return {"accounts": _suggested_cache["accounts"][:limit], "cached": True}
|
return {"accounts": _suggested_cache["accounts"][:limit], "cached": True}
|
||||||
|
|
||||||
# Load stored credentials
|
# Load stored credentials
|
||||||
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
cookies, user_agent = PlaywrightManager.load_stored_credentials()
|
||||||
|
|
||||||
if not cookies:
|
if not cookies:
|
||||||
# Return fallback static list if not authenticated
|
# Return fallback static list if not authenticated
|
||||||
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
|
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
|
||||||
|
|
||||||
print("Fetching fresh suggested accounts from TikTok...")
|
print("Fetching fresh suggested accounts from TikTok...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
accounts = await PlaywrightManager.fetch_suggested_accounts(cookies, user_agent, limit)
|
accounts = await PlaywrightManager.fetch_suggested_accounts(cookies, user_agent, limit)
|
||||||
|
|
||||||
if accounts and len(accounts) >= 5: # Need at least 5 accounts from dynamic fetch
|
if accounts and len(accounts) >= 5: # Need at least 5 accounts from dynamic fetch
|
||||||
_suggested_cache["accounts"] = accounts
|
_suggested_cache["accounts"] = accounts
|
||||||
_suggested_cache["updated_at"] = time.time()
|
_suggested_cache["updated_at"] = time.time()
|
||||||
return {"accounts": accounts[:limit], "cached": False}
|
return {"accounts": accounts[:limit], "cached": False}
|
||||||
else:
|
else:
|
||||||
# Fallback: fetch actual profile data with avatars for static list
|
# Fallback: fetch actual profile data with avatars for static list
|
||||||
print("Dynamic fetch failed, fetching profile data for static accounts...")
|
print("Dynamic fetch failed, fetching profile data for static accounts...")
|
||||||
fallback_list = get_fallback_accounts()[:min(limit, 20)] # Limit to 20 for speed
|
fallback_list = get_fallback_accounts()[:min(limit, 20)] # Limit to 20 for speed
|
||||||
return await fetch_profiles_with_avatars(fallback_list, cookies, user_agent)
|
return await fetch_profiles_with_avatars(fallback_list, cookies, user_agent)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching suggested accounts: {e}")
|
print(f"Error fetching suggested accounts: {e}")
|
||||||
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
|
return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True}
|
||||||
|
|
||||||
|
|
||||||
async def fetch_profiles_with_avatars(accounts: list, cookies: list, user_agent: str) -> dict:
|
async def fetch_profiles_with_avatars(accounts: list, cookies: list, user_agent: str) -> dict:
|
||||||
"""Fetch actual profile data with avatars for a list of accounts."""
|
"""Fetch actual profile data with avatars for a list of accounts."""
|
||||||
|
|
||||||
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies])
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
"User-Agent": user_agent or PlaywrightManager.DEFAULT_USER_AGENT,
|
||||||
"Referer": "https://www.tiktok.com/",
|
"Referer": "https://www.tiktok.com/",
|
||||||
"Cookie": cookie_str,
|
"Cookie": cookie_str,
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
enriched = []
|
enriched = []
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
for acc in accounts:
|
for acc in accounts:
|
||||||
try:
|
try:
|
||||||
url = f"https://www.tiktok.com/api/user/detail/?uniqueId={acc['username']}"
|
url = f"https://www.tiktok.com/api/user/detail/?uniqueId={acc['username']}"
|
||||||
res = await client.get(url, headers=headers)
|
res = await client.get(url, headers=headers)
|
||||||
|
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
data = res.json()
|
data = res.json()
|
||||||
user = data.get("userInfo", {}).get("user", {})
|
user = data.get("userInfo", {}).get("user", {})
|
||||||
stats = data.get("userInfo", {}).get("stats", {})
|
stats = data.get("userInfo", {}).get("stats", {})
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
enriched.append({
|
enriched.append({
|
||||||
"username": acc["username"],
|
"username": acc["username"],
|
||||||
"nickname": user.get("nickname") or acc.get("nickname", acc["username"]),
|
"nickname": user.get("nickname") or acc.get("nickname", acc["username"]),
|
||||||
"avatar": user.get("avatarThumb") or user.get("avatarMedium"),
|
"avatar": user.get("avatarThumb") or user.get("avatarMedium"),
|
||||||
"followers": stats.get("followerCount", 0),
|
"followers": stats.get("followerCount", 0),
|
||||||
"verified": user.get("verified", False),
|
"verified": user.get("verified", False),
|
||||||
"region": "VN"
|
"region": "VN"
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error fetching profile for {acc['username']}: {e}")
|
print(f"Error fetching profile for {acc['username']}: {e}")
|
||||||
|
|
||||||
# Fallback: use original data without avatar
|
# Fallback: use original data without avatar
|
||||||
enriched.append(acc)
|
enriched.append(acc)
|
||||||
|
|
||||||
return {"accounts": enriched, "cached": False, "enriched": True}
|
return {"accounts": enriched, "cached": False, "enriched": True}
|
||||||
|
|
||||||
|
|
||||||
def get_fallback_accounts():
|
def get_fallback_accounts():
|
||||||
"""Static fallback list of popular Vietnamese TikTokers (verified usernames)."""
|
"""Static fallback list of popular Vietnamese TikTokers (verified usernames)."""
|
||||||
return [
|
return [
|
||||||
# Verified Vietnamese TikTok accounts
|
# Verified Vietnamese TikTok accounts
|
||||||
{"username": "cciinnn", "nickname": "👑 CiiN (Bùi Thảo Ly)", "region": "VN"},
|
{"username": "cciinnn", "nickname": "👑 CiiN (Bùi Thảo Ly)", "region": "VN"},
|
||||||
{"username": "hoaa.hanassii", "nickname": "💃 Hoa Hanassii", "region": "VN"},
|
{"username": "hoaa.hanassii", "nickname": "💃 Hoa Hanassii", "region": "VN"},
|
||||||
{"username": "lebong95", "nickname": "💪 Lê Bống", "region": "VN"},
|
{"username": "lebong95", "nickname": "💪 Lê Bống", "region": "VN"},
|
||||||
{"username": "tieu_hy26", "nickname": "👰 Tiểu Hý", "region": "VN"},
|
{"username": "tieu_hy26", "nickname": "👰 Tiểu Hý", "region": "VN"},
|
||||||
{"username": "hieuthuhai2222", "nickname": "🎧 HIEUTHUHAI", "region": "VN"},
|
{"username": "hieuthuhai2222", "nickname": "🎧 HIEUTHUHAI", "region": "VN"},
|
||||||
{"username": "mtp.fan", "nickname": "🎤 Sơn Tùng M-TP", "region": "VN"},
|
{"username": "mtp.fan", "nickname": "🎤 Sơn Tùng M-TP", "region": "VN"},
|
||||||
{"username": "changmakeup", "nickname": "💄 Changmakeup", "region": "VN"},
|
{"username": "changmakeup", "nickname": "💄 Changmakeup", "region": "VN"},
|
||||||
{"username": "theanh28entertainment", "nickname": "🎬 Theanh28", "region": "VN"},
|
{"username": "theanh28entertainment", "nickname": "🎬 Theanh28", "region": "VN"},
|
||||||
{"username": "linhbarbie", "nickname": "👗 Linh Barbie", "region": "VN"},
|
{"username": "linhbarbie", "nickname": "👗 Linh Barbie", "region": "VN"},
|
||||||
{"username": "phuonglykchau", "nickname": "✨ Phương Ly", "region": "VN"},
|
{"username": "phuonglykchau", "nickname": "✨ Phương Ly", "region": "VN"},
|
||||||
{"username": "phimtieutrang", "nickname": "📺 Tiểu Trang", "region": "VN"},
|
{"username": "phimtieutrang", "nickname": "📺 Tiểu Trang", "region": "VN"},
|
||||||
{"username": "nhunguyendy", "nickname": "💕 Như Nguyễn", "region": "VN"},
|
{"username": "nhunguyendy", "nickname": "💕 Như Nguyễn", "region": "VN"},
|
||||||
{"username": "trucnhantv", "nickname": "🎤 Trúc Nhân", "region": "VN"},
|
{"username": "trucnhantv", "nickname": "🎤 Trúc Nhân", "region": "VN"},
|
||||||
{"username": "justvietanh", "nickname": "😄 Just Việt Anh", "region": "VN"},
|
{"username": "justvietanh", "nickname": "😄 Just Việt Anh", "region": "VN"},
|
||||||
{"username": "minngu.official", "nickname": "🌸 Min NGU", "region": "VN"},
|
{"username": "minngu.official", "nickname": "🌸 Min NGU", "region": "VN"},
|
||||||
{"username": "quangdangofficial", "nickname": "🕺 Quang Đăng", "region": "VN"},
|
{"username": "quangdangofficial", "nickname": "🕺 Quang Đăng", "region": "VN"},
|
||||||
{"username": "minhhangofficial", "nickname": "👑 Minh Hằng", "region": "VN"},
|
{"username": "minhhangofficial", "nickname": "👑 Minh Hằng", "region": "VN"},
|
||||||
{"username": "dungntt", "nickname": "🎭 Dũng NTT", "region": "VN"},
|
{"username": "dungntt", "nickname": "🎭 Dũng NTT", "region": "VN"},
|
||||||
{"username": "chipu88", "nickname": "🎤 Chi Pu", "region": "VN"},
|
{"username": "chipu88", "nickname": "🎤 Chi Pu", "region": "VN"},
|
||||||
{"username": "kaydinh", "nickname": "🎵 Kay Dinh", "region": "VN"},
|
{"username": "kaydinh", "nickname": "🎵 Kay Dinh", "region": "VN"},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
164
backend/main.py
164
backend/main.py
|
|
@ -1,82 +1,82 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from api.routes import auth, feed, download, following, config, user
|
from api.routes import auth, feed, download, following, config, user
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Force Proactor on Windows for Playwright
|
# Force Proactor on Windows for Playwright
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Startup and shutdown events."""
|
"""Startup and shutdown events."""
|
||||||
print("🚀 Starting PureStream API (Network Interception Mode)...")
|
print("🚀 Starting PureStream API (Network Interception Mode)...")
|
||||||
import asyncio
|
import asyncio
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
print(f"DEBUG: Running event loop: {type(loop)}")
|
print(f"DEBUG: Running event loop: {type(loop)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"DEBUG: Could not get running loop: {e}")
|
print(f"DEBUG: Could not get running loop: {e}")
|
||||||
yield
|
yield
|
||||||
print("👋 Shutting down PureStream API...")
|
print("👋 Shutting down PureStream API...")
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
app = FastAPI(title="PureStream API", version="2.0.0", lifespan=lifespan)
|
app = FastAPI(title="PureStream API", version="2.0.0", lifespan=lifespan)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
print(f"DEBUG: Current event loop: {type(loop)}")
|
print(f"DEBUG: Current event loop: {type(loop)}")
|
||||||
except:
|
except:
|
||||||
print("DEBUG: No event loop yet")
|
print("DEBUG: No event loop yet")
|
||||||
|
|
||||||
|
|
||||||
# CORS middleware
|
# CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", "*"],
|
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", "*"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
||||||
app.include_router(feed.router, prefix="/api/feed", tags=["Feed"])
|
app.include_router(feed.router, prefix="/api/feed", tags=["Feed"])
|
||||||
app.include_router(download.router, prefix="/api/download", tags=["Download"])
|
app.include_router(download.router, prefix="/api/download", tags=["Download"])
|
||||||
app.include_router(following.router, prefix="/api/following", tags=["Following"])
|
app.include_router(following.router, prefix="/api/following", tags=["Following"])
|
||||||
app.include_router(config.router, prefix="/api/config", tags=["Config"])
|
app.include_router(config.router, prefix="/api/config", tags=["Config"])
|
||||||
app.include_router(user.router, prefix="/api/user", tags=["User"])
|
app.include_router(user.router, prefix="/api/user", tags=["User"])
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
# Serve static frontend files in production
|
# Serve static frontend files in production
|
||||||
FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
|
FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
|
||||||
if FRONTEND_DIR.exists():
|
if FRONTEND_DIR.exists():
|
||||||
# Mount static assets
|
# Mount static assets
|
||||||
app.mount("/assets", StaticFiles(directory=FRONTEND_DIR / "assets"), name="assets")
|
app.mount("/assets", StaticFiles(directory=FRONTEND_DIR / "assets"), name="assets")
|
||||||
|
|
||||||
# Serve index.html for all non-API routes (SPA fallback)
|
# Serve index.html for all non-API routes (SPA fallback)
|
||||||
@app.get("/{full_path:path}")
|
@app.get("/{full_path:path}")
|
||||||
async def serve_spa(full_path: str):
|
async def serve_spa(full_path: str):
|
||||||
# If requesting a file that exists, serve it
|
# If requesting a file that exists, serve it
|
||||||
file_path = FRONTEND_DIR / full_path
|
file_path = FRONTEND_DIR / full_path
|
||||||
if file_path.is_file():
|
if file_path.is_file():
|
||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
# Otherwise serve index.html for SPA routing
|
# Otherwise serve index.html for SPA routing
|
||||||
return FileResponse(FRONTEND_DIR / "index.html")
|
return FileResponse(FRONTEND_DIR / "index.html")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=False, loop="asyncio")
|
uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=False, loop="asyncio")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Fix sys.path for user site-packages where pip installed dependencies
|
# Fix sys.path for user site-packages where pip installed dependencies
|
||||||
user_site = os.path.expanduser("~\\AppData\\Roaming\\Python\\Python312\\site-packages")
|
user_site = os.path.expanduser("~\\AppData\\Roaming\\Python\\Python312\\site-packages")
|
||||||
if os.path.exists(user_site) and user_site not in sys.path:
|
if os.path.exists(user_site) and user_site not in sys.path:
|
||||||
print(f"DEBUG: Adding user site-packages to path: {user_site}")
|
print(f"DEBUG: Adding user site-packages to path: {user_site}")
|
||||||
sys.path.append(user_site)
|
sys.path.append(user_site)
|
||||||
|
|
||||||
# Enforce ProactorEventLoopPolicy for Playwright on Windows
|
# Enforce ProactorEventLoopPolicy for Playwright on Windows
|
||||||
# This is required for asyncio.create_subprocess_exec used by Playwright
|
# This is required for asyncio.create_subprocess_exec used by Playwright
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
# Check if policy is already set
|
# Check if policy is already set
|
||||||
current_policy = asyncio.get_event_loop_policy()
|
current_policy = asyncio.get_event_loop_policy()
|
||||||
if not isinstance(current_policy, asyncio.WindowsProactorEventLoopPolicy):
|
if not isinstance(current_policy, asyncio.WindowsProactorEventLoopPolicy):
|
||||||
print("DEBUG: Setting WindowsProactorEventLoopPolicy")
|
print("DEBUG: Setting WindowsProactorEventLoopPolicy")
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
else:
|
else:
|
||||||
print("DEBUG: WindowsProactorEventLoopPolicy already active")
|
print("DEBUG: WindowsProactorEventLoopPolicy already active")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
print("🚀 Bootstrapping Uvicorn with Proactor Loop (Reload Disabled)...")
|
print("🚀 Bootstrapping Uvicorn with Proactor Loop (Reload Disabled)...")
|
||||||
uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=False, loop="asyncio")
|
uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=False, loop="asyncio")
|
||||||
|
|
|
||||||
622
cookies.json
622
cookies.json
|
|
@ -1,312 +1,312 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1784039026,
|
"expirationDate": 1784039026,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "delay_guest_mode_vid",
|
"name": "delay_guest_mode_vid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "8"
|
"value": "8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1768142590.076948,
|
"expirationDate": 1768142590.076948,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "msToken",
|
"name": "msToken",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "qKUFutg-Q184Mo5kpuAm4XvPCCucGe3O2KXf5G9pHV61Hb9puK-ZVQ7XzexuVGLLzwmFZ1mVYOgR3QbKBXk58AX9UgPPPkWk_koDZF3e-gqQGg_9GGjcdIOxGN-JTL_g0FM4qN8NKV84LdU="
|
"value": "qKUFutg-Q184Mo5kpuAm4XvPCCucGe3O2KXf5G9pHV61Hb9puK-ZVQ7XzexuVGLLzwmFZ1mVYOgR3QbKBXk58AX9UgPPPkWk_koDZF3e-gqQGg_9GGjcdIOxGN-JTL_g0FM4qN8NKV84LdU="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646103,
|
"expirationDate": 1782817620.646103,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_session_tlb_tag",
|
"name": "tt_session_tlb_tag",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "sttt%7C1%7C6M35zM57kkqAGSs_LSUdRP_________zyplxYaEARSr2PNU_6cKcB0lq4WRz1GKKY43u399i5hs%3D"
|
"value": "sttt%7C1%7C6M35zM57kkqAGSs_LSUdRP_________zyplxYaEARSr2PNU_6cKcB0lq4WRz1GKKY43u399i5hs%3D"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1798369620.645922,
|
"expirationDate": 1798369620.645922,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_guard",
|
"name": "sid_guard",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44%7C1767265616%7C15552000%7CTue%2C+30-Jun-2026+11%3A06%3A56+GMT"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44%7C1767265616%7C15552000%7CTue%2C+30-Jun-2026+11%3A06%3A56+GMT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1798814589.722864,
|
"expirationDate": 1798814589.722864,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "ttwid",
|
"name": "ttwid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1%7CAYDsetgnxt5vzYX8hD6Wq2DQ4FXiL_pqcdLwHWwz6B8%7C1767278585%7C46a4b0b8d2fc0ee903d0e781593a0d2d7b491a49752c86f034adf69319d371a0"
|
"value": "1%7CAYDsetgnxt5vzYX8hD6Wq2DQ4FXiL_pqcdLwHWwz6B8%7C1767278585%7C46a4b0b8d2fc0ee903d0e781593a0d2d7b491a49752c86f034adf69319d371a0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1767883392,
|
"expirationDate": 1767883392,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "perf_feed_cache",
|
"name": "perf_feed_cache",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "{%22expireTimestamp%22:0%2C%22itemIds%22:[%22%22%2C%227584357225863335188%22%2C%227580369401002659079%22]}"
|
"value": "{%22expireTimestamp%22:0%2C%22itemIds%22:[%22%22%2C%227584357225863335188%22%2C%227580369401002659079%22]}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.645952,
|
"expirationDate": 1782817620.645952,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "uid_tt",
|
"name": "uid_tt",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449601.742227,
|
"expirationDate": 1772449601.742227,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "passport_csrf_token_default",
|
"name": "passport_csrf_token_default",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "s_v_web_id",
|
"name": "s_v_web_id",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": true,
|
"session": true,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "verify_mjvcbi31_l3mxUEeU_ykis_4x6z_859S_8zEFmNseEnJU"
|
"value": "verify_mjvcbi31_l3mxUEeU_ykis_4x6z_859S_8zEFmNseEnJU"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.64617,
|
"expirationDate": 1782817620.64617,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "ssid_ucp_v1",
|
"name": "ssid_ucp_v1",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1793198587,
|
"expirationDate": 1793198587,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "tiktok_webapp_theme",
|
"name": "tiktok_webapp_theme",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "dark"
|
"value": "dark"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1799409936.219767,
|
"expirationDate": 1799409936.219767,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "_ttp",
|
"name": "_ttp",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "32XOXKxwj8YLtBQf0OBn4TvlkPN"
|
"value": "32XOXKxwj8YLtBQf0OBn4TvlkPN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449620.645821,
|
"expirationDate": 1772449620.645821,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "cmpl_token",
|
"name": "cmpl_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "AgQYAPOF_hfkTtKPtFExgPKdOPKrXVkNUj-FDmCi6K4"
|
"value": "AgQYAPOF_hfkTtKPtFExgPKdOPKrXVkNUj-FDmCi6K4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449620.645628,
|
"expirationDate": 1772449620.645628,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "multi_sids",
|
"name": "multi_sids",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "6884525631502042113%3Ae8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "6884525631502042113%3Ae8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1769857620.645892,
|
"expirationDate": 1769857620.645892,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "passport_auth_status_ss",
|
"name": "passport_auth_status_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "966972581a398dbb9ead189c044cf98c%2C"
|
"value": "966972581a398dbb9ead189c044cf98c%2C"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449601.742082,
|
"expirationDate": 1772449601.742082,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "passport_csrf_token",
|
"name": "passport_csrf_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646041,
|
"expirationDate": 1782817620.646041,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sessionid",
|
"name": "sessionid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646073,
|
"expirationDate": 1782817620.646073,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sessionid_ss",
|
"name": "sessionid_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646008,
|
"expirationDate": 1782817620.646008,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_tt",
|
"name": "sid_tt",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646132,
|
"expirationDate": 1782817620.646132,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_ucp_v1",
|
"name": "sid_ucp_v1",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1793198587,
|
"expirationDate": 1793198587,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "tiktok_webapp_theme_source",
|
"name": "tiktok_webapp_theme_source",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "auto"
|
"value": "auto"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782830586.65306,
|
"expirationDate": 1782830586.65306,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_chain_token",
|
"name": "tt_chain_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "6deMEWrkAGUe9R0tCISIoQ=="
|
"value": "6deMEWrkAGUe9R0tCISIoQ=="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_csrf_token",
|
"name": "tt_csrf_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "lax",
|
"sameSite": "lax",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": true,
|
"session": true,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "q0Q4ki72-I7zQB6eLbpBBaqFBGrUF_v85N9s"
|
"value": "q0Q4ki72-I7zQB6eLbpBBaqFBGrUF_v85N9s"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.645979,
|
"expirationDate": 1782817620.645979,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "uid_tt_ss",
|
"name": "uid_tt_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
360
frontend/package-lock.json
generated
360
frontend/package-lock.json
generated
|
|
@ -81,7 +81,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -725,9 +724,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -1038,9 +1037,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||||
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
|
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1052,9 +1051,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
||||||
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
|
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1066,9 +1065,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1080,9 +1079,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
||||||
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
|
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1094,9 +1093,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
||||||
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
|
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1108,9 +1107,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
||||||
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
|
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1122,9 +1121,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
||||||
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
|
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1136,9 +1135,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
||||||
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
|
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -1150,9 +1149,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
|
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1164,9 +1163,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
||||||
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
|
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1178,9 +1177,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
|
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
|
@ -1192,9 +1191,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
|
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
|
@ -1206,9 +1205,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
|
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
|
@ -1220,9 +1219,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
||||||
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
|
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
|
@ -1234,9 +1233,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
|
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
|
@ -1248,9 +1247,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
|
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1262,9 +1261,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
||||||
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
|
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1276,9 +1275,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
||||||
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
|
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1290,9 +1289,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
||||||
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
|
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -1304,9 +1303,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
||||||
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
|
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
|
@ -1318,9 +1317,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
||||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1332,9 +1331,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
||||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -1410,7 +1409,6 @@
|
||||||
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1428,7 +1426,6 @@
|
||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -1445,20 +1442,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
|
||||||
"integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==",
|
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.50.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/type-utils": "8.50.0",
|
"@typescript-eslint/type-utils": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.0",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^7.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1468,7 +1465,7 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.50.0",
|
"@typescript-eslint/parser": "^8.51.0",
|
||||||
"eslint": "^8.57.0 || ^9.0.0",
|
"eslint": "^8.57.0 || ^9.0.0",
|
||||||
"typescript": ">=4.8.4 <6.0.0"
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1484,17 +1481,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
|
||||||
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
|
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.50.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1510,14 +1506,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
|
||||||
"integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==",
|
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.50.0",
|
"@typescript-eslint/tsconfig-utils": "^8.51.0",
|
||||||
"@typescript-eslint/types": "^8.50.0",
|
"@typescript-eslint/types": "^8.51.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1532,14 +1528,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
|
||||||
"integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==",
|
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.0"
|
"@typescript-eslint/visitor-keys": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1550,9 +1546,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==",
|
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1567,17 +1563,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==",
|
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.0",
|
"@typescript-eslint/utils": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1592,9 +1588,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
|
||||||
"integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==",
|
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1606,21 +1602,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
|
||||||
"integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==",
|
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.50.0",
|
"@typescript-eslint/project-service": "8.51.0",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.50.0",
|
"@typescript-eslint/tsconfig-utils": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.50.0",
|
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"tinyglobby": "^0.2.15",
|
"tinyglobby": "^0.2.15",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1673,16 +1669,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
|
||||||
"integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==",
|
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.7.0",
|
||||||
"@typescript-eslint/scope-manager": "8.50.0",
|
"@typescript-eslint/scope-manager": "8.51.0",
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.0"
|
"@typescript-eslint/typescript-estree": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -1697,13 +1693,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
|
||||||
"integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==",
|
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.50.0",
|
"@typescript-eslint/types": "8.51.0",
|
||||||
"eslint-visitor-keys": "^4.2.1"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1741,7 +1737,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1898,9 +1893,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.9",
|
"version": "2.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.9.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||||
"integrity": "sha512-V8fbOCSeOFvlDj7LLChUcqbZrdKD9RU/VR260piF1790vT0mfLSwGc/Qzxv3IqiTukOpNtItePa0HBpMAj7MDg==",
|
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -1964,7 +1959,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -2013,9 +2007,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001760",
|
"version": "1.0.30001762",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||||
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
|
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -2370,7 +2364,6 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -2497,9 +2490,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2594,9 +2587,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -3014,7 +3007,6 @@
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
|
|
@ -3515,7 +3507,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -3711,7 +3702,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -3724,7 +3714,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
|
|
@ -3841,9 +3830,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.53.5",
|
"version": "4.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||||
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
|
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -3857,28 +3846,28 @@
|
||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.53.5",
|
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
||||||
"@rollup/rollup-android-arm64": "4.53.5",
|
"@rollup/rollup-android-arm64": "4.54.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.53.5",
|
"@rollup/rollup-darwin-arm64": "4.54.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.53.5",
|
"@rollup/rollup-darwin-x64": "4.54.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.53.5",
|
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.53.5",
|
"@rollup/rollup-freebsd-x64": "4.54.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
|
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.53.5",
|
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
|
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
|
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
|
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
|
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
|
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.53.5",
|
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.53.5",
|
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.53.5",
|
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
|
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
|
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.53.5",
|
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.53.5",
|
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -4132,7 +4121,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -4154,9 +4142,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz",
|
||||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
"integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -4198,7 +4186,6 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -4208,16 +4195,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.50.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz",
|
||||||
"integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==",
|
"integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
"@typescript-eslint/eslint-plugin": "8.51.0",
|
||||||
"@typescript-eslint/parser": "8.50.0",
|
"@typescript-eslint/parser": "8.51.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.50.0",
|
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||||
"@typescript-eslint/utils": "8.50.0"
|
"@typescript-eslint/utils": "8.51.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -4292,7 +4279,6 @@
|
||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
|
||||||
{/* Search Icon / Toggle */}
|
{/* Search Icon / Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className={`text-white p-3 hover:text-pink-500 transition-colors duration-300 ${isOpen ? '' : 'w-full h-full flex items-center justify-center'}`}
|
className={`text-white p-3 hover:text-white transition-colors duration-300 ${isOpen ? '' : 'w-full h-full flex items-center justify-center'}`}
|
||||||
>
|
>
|
||||||
<Search size={isOpen ? 18 : 20} className={isOpen ? 'opacity-60' : ''} />
|
<Search size={isOpen ? 18 : 20} className={isOpen ? 'opacity-60' : ''} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -41,7 +41,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-pink-500/10 to-violet-500/10 pointer-events-none" />
|
<div className="absolute inset-0 bg-gradient-to-r from-gray-400/10 to-gray-300/10 pointer-events-none" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,51 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface SearchSkeletonProps {
|
interface SearchSkeletonProps {
|
||||||
count?: number;
|
count?: number;
|
||||||
estimatedTime?: number;
|
estimatedTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchSkeleton: React.FC<SearchSkeletonProps> = ({
|
export const SearchSkeleton: React.FC<SearchSkeletonProps> = ({
|
||||||
count = 9,
|
count = 9,
|
||||||
estimatedTime
|
estimatedTime
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Countdown Timer */}
|
{/* Countdown Timer */}
|
||||||
{estimatedTime !== undefined && estimatedTime > 0 && (
|
{estimatedTime !== undefined && estimatedTime > 0 && (
|
||||||
<div className="text-center py-2">
|
<div className="text-center py-2">
|
||||||
<p className="text-white/50 text-xs">
|
<p className="text-white/50 text-xs">
|
||||||
Estimated time: ~{Math.ceil(estimatedTime)}s
|
Estimated time: ~{Math.ceil(estimatedTime)}s
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skeleton Grid */}
|
{/* Skeleton Grid */}
|
||||||
<div className="grid grid-cols-3 gap-1">
|
<div className="grid grid-cols-3 gap-1">
|
||||||
{Array.from({ length: count }).map((_, index) => (
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="aspect-[9/16] bg-white/5 rounded-lg animate-pulse relative overflow-hidden"
|
className="aspect-[9/16] bg-white/5 rounded-lg animate-pulse relative overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${index * 100}ms`
|
animationDelay: `${index * 100}ms`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Shimmer effect */}
|
{/* Shimmer effect */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent shimmer" />
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent shimmer" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add shimmer keyframes via inline style */}
|
{/* Add shimmer keyframes via inline style */}
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { transform: translateX(-100%); }
|
0% { transform: translateX(-100%); }
|
||||||
100% { transform: translateX(100%); }
|
100% { transform: translateX(100%); }
|
||||||
}
|
}
|
||||||
.shimmer {
|
.shimmer {
|
||||||
animation: shimmer 1.5s infinite;
|
animation: shimmer 1.5s infinite;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,12 @@
|
||||||
#root {
|
#root {
|
||||||
@apply h-full overflow-hidden;
|
@apply h-full overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile-safe full height - accounts for browser chrome */
|
||||||
|
.h-screen-safe {
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
|
||||||
|
|
@ -1,148 +1,148 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { Video } from '../types';
|
import type { Video } from '../types';
|
||||||
import { API_BASE_URL } from '../config';
|
import { API_BASE_URL } from '../config';
|
||||||
|
|
||||||
interface FeedStats {
|
interface FeedStats {
|
||||||
totalLoaded: number;
|
totalLoaded: number;
|
||||||
loadTime: number;
|
loadTime: number;
|
||||||
batchSize: number;
|
batchSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeedLoader {
|
class FeedLoader {
|
||||||
private stats: FeedStats = {
|
private stats: FeedStats = {
|
||||||
totalLoaded: 0,
|
totalLoaded: 0,
|
||||||
loadTime: 0,
|
loadTime: 0,
|
||||||
batchSize: 12
|
batchSize: 12
|
||||||
};
|
};
|
||||||
|
|
||||||
private requestCache: Map<string, { data: Video[]; timestamp: number }> = new Map();
|
private requestCache: Map<string, { data: Video[]; timestamp: number }> = new Map();
|
||||||
private CACHE_TTL_MS = 60000;
|
private CACHE_TTL_MS = 60000;
|
||||||
|
|
||||||
async loadFeedWithOptimization(
|
async loadFeedWithOptimization(
|
||||||
fast: boolean = false,
|
fast: boolean = false,
|
||||||
onProgress?: (videos: Video[]) => void,
|
onProgress?: (videos: Video[]) => void,
|
||||||
skipCache: boolean = false
|
skipCache: boolean = false
|
||||||
): Promise<Video[]> {
|
): Promise<Video[]> {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (fast && !skipCache) {
|
if (fast && !skipCache) {
|
||||||
const videos = await this.loadWithCache('feed-fast');
|
const videos = await this.loadWithCache('feed-fast');
|
||||||
onProgress?.(videos);
|
onProgress?.(videos);
|
||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = 'feed-full';
|
const cacheKey = 'feed-full';
|
||||||
|
|
||||||
// Skip cache check when explicitly requested (for infinite scroll)
|
// Skip cache check when explicitly requested (for infinite scroll)
|
||||||
if (!skipCache) {
|
if (!skipCache) {
|
||||||
const cached = this.getCached(cacheKey);
|
const cached = this.getCached(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
onProgress?.(cached);
|
onProgress?.(cached);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const videos = await this.fetchFeed(skipCache);
|
const videos = await this.fetchFeed(skipCache);
|
||||||
|
|
||||||
// Only cache if not skipping (initial load)
|
// Only cache if not skipping (initial load)
|
||||||
if (!skipCache) {
|
if (!skipCache) {
|
||||||
this.setCached(cacheKey, videos);
|
this.setCached(cacheKey, videos);
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress?.(videos);
|
onProgress?.(videos);
|
||||||
|
|
||||||
this.stats.loadTime = performance.now() - startTime;
|
this.stats.loadTime = performance.now() - startTime;
|
||||||
this.stats.totalLoaded = videos.length;
|
this.stats.totalLoaded = videos.length;
|
||||||
|
|
||||||
return videos;
|
return videos;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Feed load failed:', error);
|
console.error('Feed load failed:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchFeed(skipCache: boolean = false): Promise<Video[]> {
|
private async fetchFeed(skipCache: boolean = false): Promise<Video[]> {
|
||||||
// Add skip_cache parameter to force backend to fetch fresh videos
|
// Add skip_cache parameter to force backend to fetch fresh videos
|
||||||
const url = skipCache
|
const url = skipCache
|
||||||
? `${API_BASE_URL}/feed?skip_cache=true`
|
? `${API_BASE_URL}/feed?skip_cache=true`
|
||||||
: `${API_BASE_URL}/feed`;
|
: `${API_BASE_URL}/feed`;
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
|
|
||||||
if (!Array.isArray(response.data)) {
|
if (!Array.isArray(response.data)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data.map((v: any, i: number) => ({
|
return response.data.map((v: any, i: number) => ({
|
||||||
id: v.id || `video-${i}`,
|
id: v.id || `video-${i}`,
|
||||||
url: v.url,
|
url: v.url,
|
||||||
author: v.author || 'unknown',
|
author: v.author || 'unknown',
|
||||||
description: v.description || '',
|
description: v.description || '',
|
||||||
thumbnail: v.thumbnail,
|
thumbnail: v.thumbnail,
|
||||||
cdn_url: v.cdn_url,
|
cdn_url: v.cdn_url,
|
||||||
views: v.views,
|
views: v.views,
|
||||||
likes: v.likes
|
likes: v.likes
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadWithCache(key: string): Promise<Video[]> {
|
private async loadWithCache(key: string): Promise<Video[]> {
|
||||||
const cached = this.getCached(key);
|
const cached = this.getCached(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const videos = await this.fetchFeed();
|
const videos = await this.fetchFeed();
|
||||||
this.setCached(key, videos);
|
this.setCached(key, videos);
|
||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCached(key: string): Video[] | null {
|
private getCached(key: string): Video[] | null {
|
||||||
const cached = this.requestCache.get(key);
|
const cached = this.requestCache.get(key);
|
||||||
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL_MS) {
|
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL_MS) {
|
||||||
return cached.data;
|
return cached.data;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCached(key: string, data: Video[]): void {
|
private setCached(key: string, data: Video[]): void {
|
||||||
this.requestCache.set(key, {
|
this.requestCache.set(key, {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats(): FeedStats {
|
getStats(): FeedStats {
|
||||||
return { ...this.stats };
|
return { ...this.stats };
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache(): void {
|
clearCache(): void {
|
||||||
this.requestCache.clear();
|
this.requestCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptimalBatchSize(): number {
|
getOptimalBatchSize(): number {
|
||||||
const connection = (navigator as any).connection;
|
const connection = (navigator as any).connection;
|
||||||
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return 15;
|
return 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveType = connection.effectiveType;
|
const effectiveType = connection.effectiveType;
|
||||||
|
|
||||||
switch (effectiveType) {
|
switch (effectiveType) {
|
||||||
case '4g':
|
case '4g':
|
||||||
return 20;
|
return 20;
|
||||||
case '3g':
|
case '3g':
|
||||||
return 12;
|
return 12;
|
||||||
case '2g':
|
case '2g':
|
||||||
return 6;
|
return 6;
|
||||||
default:
|
default:
|
||||||
return 15;
|
return 15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldPrefetchThumbnails(): boolean {
|
shouldPrefetchThumbnails(): boolean {
|
||||||
const connection = (navigator as any).connection;
|
const connection = (navigator as any).connection;
|
||||||
if (!connection) return true;
|
if (!connection) return true;
|
||||||
return connection.saveData !== true;
|
return connection.saveData !== true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const feedLoader = new FeedLoader();
|
export const feedLoader = new FeedLoader();
|
||||||
|
|
|
||||||
|
|
@ -1,207 +1,207 @@
|
||||||
interface CachedVideo {
|
interface CachedVideo {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
data: Blob;
|
data: Blob;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DB_NAME = 'PureStreamCache';
|
const DB_NAME = 'PureStreamCache';
|
||||||
const STORE_NAME = 'videos';
|
const STORE_NAME = 'videos';
|
||||||
const MAX_CACHE_SIZE_MB = 200;
|
const MAX_CACHE_SIZE_MB = 200;
|
||||||
const CACHE_TTL_HOURS = 24;
|
const CACHE_TTL_HOURS = 24;
|
||||||
|
|
||||||
class VideoCache {
|
class VideoCache {
|
||||||
private db: IDBDatabase | null = null;
|
private db: IDBDatabase | null = null;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(DB_NAME, 1);
|
const request = indexedDB.open(DB_NAME, 1);
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
request.onerror = () => reject(request.error);
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
this.db = request.result;
|
this.db = request.result;
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onupgradeneeded = (e) => {
|
request.onupgradeneeded = (e) => {
|
||||||
const db = (e.target as IDBOpenDBRequest).result;
|
const db = (e.target as IDBOpenDBRequest).result;
|
||||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(url: string): Promise<Blob | null> {
|
async get(url: string): Promise<Blob | null> {
|
||||||
if (!this.db) await this.init();
|
if (!this.db) await this.init();
|
||||||
if (!this.db) return null;
|
if (!this.db) return null;
|
||||||
|
|
||||||
const videoId = this.getVideoId(url);
|
const videoId = this.getVideoId(url);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readonly');
|
const tx = this.db!.transaction(STORE_NAME, 'readonly');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.get(videoId);
|
const request = store.get(videoId);
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const cached = request.result as CachedVideo | undefined;
|
const cached = request.result as CachedVideo | undefined;
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const ageHours = (Date.now() - cached.timestamp) / (1000 * 60 * 60);
|
const ageHours = (Date.now() - cached.timestamp) / (1000 * 60 * 60);
|
||||||
if (ageHours < CACHE_TTL_HOURS) {
|
if (ageHours < CACHE_TTL_HOURS) {
|
||||||
resolve(cached.data);
|
resolve(cached.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.delete(videoId);
|
this.delete(videoId);
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => resolve(null);
|
request.onerror = () => resolve(null);
|
||||||
} catch {
|
} catch {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(url: string, blob: Blob): Promise<void> {
|
async set(url: string, blob: Blob): Promise<void> {
|
||||||
if (!this.db) await this.init();
|
if (!this.db) await this.init();
|
||||||
if (!this.db) return;
|
if (!this.db) return;
|
||||||
|
|
||||||
const videoId = this.getVideoId(url);
|
const videoId = this.getVideoId(url);
|
||||||
const cached: CachedVideo = {
|
const cached: CachedVideo = {
|
||||||
id: videoId,
|
id: videoId,
|
||||||
url,
|
url,
|
||||||
data: blob,
|
data: blob,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
size: blob.size
|
size: blob.size
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.put(cached);
|
const request = store.put(cached);
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => resolve();
|
request.onerror = () => resolve();
|
||||||
} catch {
|
} catch {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(videoId: string): Promise<void> {
|
async delete(videoId: string): Promise<void> {
|
||||||
if (!this.db) return;
|
if (!this.db) return;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.delete(videoId);
|
const request = store.delete(videoId);
|
||||||
|
|
||||||
request.onsuccess = () => resolve();
|
request.onsuccess = () => resolve();
|
||||||
request.onerror = () => resolve();
|
request.onerror = () => resolve();
|
||||||
} catch {
|
} catch {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
if (!this.db) return;
|
if (!this.db) return;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.clear();
|
const request = store.clear();
|
||||||
|
|
||||||
request.onsuccess = () => resolve();
|
request.onsuccess = () => resolve();
|
||||||
request.onerror = () => resolve();
|
request.onerror = () => resolve();
|
||||||
} catch {
|
} catch {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStats(): Promise<{ size_mb: number; count: number }> {
|
async getStats(): Promise<{ size_mb: number; count: number }> {
|
||||||
if (!this.db) return { size_mb: 0, count: 0 };
|
if (!this.db) return { size_mb: 0, count: 0 };
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readonly');
|
const tx = this.db!.transaction(STORE_NAME, 'readonly');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.getAll();
|
const request = store.getAll();
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const cached = request.result as CachedVideo[];
|
const cached = request.result as CachedVideo[];
|
||||||
const totalSize = cached.reduce((sum, v) => sum + v.size, 0);
|
const totalSize = cached.reduce((sum, v) => sum + v.size, 0);
|
||||||
resolve({
|
resolve({
|
||||||
size_mb: Math.round(totalSize / 1024 / 1024 * 100) / 100,
|
size_mb: Math.round(totalSize / 1024 / 1024 * 100) / 100,
|
||||||
count: cached.length
|
count: cached.length
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => resolve({ size_mb: 0, count: 0 });
|
request.onerror = () => resolve({ size_mb: 0, count: 0 });
|
||||||
} catch {
|
} catch {
|
||||||
resolve({ size_mb: 0, count: 0 });
|
resolve({ size_mb: 0, count: 0 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cleanup(): Promise<void> {
|
private async cleanup(): Promise<void> {
|
||||||
if (!this.db) return;
|
if (!this.db) return;
|
||||||
|
|
||||||
const stats = await this.getStats();
|
const stats = await this.getStats();
|
||||||
if (stats.size_mb < MAX_CACHE_SIZE_MB) return;
|
if (stats.size_mb < MAX_CACHE_SIZE_MB) return;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
const tx = this.db!.transaction(STORE_NAME, 'readwrite');
|
||||||
const store = tx.objectStore(STORE_NAME);
|
const store = tx.objectStore(STORE_NAME);
|
||||||
const request = store.getAll();
|
const request = store.getAll();
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const cached = (request.result as CachedVideo[]).sort(
|
const cached = (request.result as CachedVideo[]).sort(
|
||||||
(a, b) => a.timestamp - b.timestamp
|
(a, b) => a.timestamp - b.timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
let totalSize = cached.reduce((sum, v) => sum + v.size, 0);
|
let totalSize = cached.reduce((sum, v) => sum + v.size, 0);
|
||||||
const targetSize = MAX_CACHE_SIZE_MB * 1024 * 1024 * 0.8;
|
const targetSize = MAX_CACHE_SIZE_MB * 1024 * 1024 * 0.8;
|
||||||
|
|
||||||
for (const video of cached) {
|
for (const video of cached) {
|
||||||
if (totalSize <= targetSize) break;
|
if (totalSize <= targetSize) break;
|
||||||
const deleteReq = store.delete(video.id);
|
const deleteReq = store.delete(video.id);
|
||||||
deleteReq.onsuccess = () => {
|
deleteReq.onsuccess = () => {
|
||||||
totalSize -= video.size;
|
totalSize -= video.size;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = () => resolve();
|
request.onerror = () => resolve();
|
||||||
} catch {
|
} catch {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVideoId(url: string): string {
|
private getVideoId(url: string): string {
|
||||||
const match = url.match(/video\/(\d+)|id=([^&]+)/);
|
const match = url.match(/video\/(\d+)|id=([^&]+)/);
|
||||||
return match ? match[1] || match[2] : url.substring(0, 50);
|
return match ? match[1] || match[2] : url.substring(0, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const videoCache = new VideoCache();
|
export const videoCache = new VideoCache();
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,128 @@
|
||||||
import { videoCache } from './videoCache';
|
import { videoCache } from './videoCache';
|
||||||
import type { Video } from '../types';
|
import type { Video } from '../types';
|
||||||
|
|
||||||
interface PrefetchConfig {
|
interface PrefetchConfig {
|
||||||
lookahead: number;
|
lookahead: number;
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG: PrefetchConfig = {
|
const DEFAULT_CONFIG: PrefetchConfig = {
|
||||||
lookahead: 3, // Increased from 2 for better buffering
|
lookahead: 3, // Increased from 2 for better buffering
|
||||||
concurrency: 2, // Increased from 1 for parallel downloads
|
concurrency: 2, // Increased from 1 for parallel downloads
|
||||||
timeoutMs: 30000
|
timeoutMs: 30000
|
||||||
};
|
};
|
||||||
|
|
||||||
class VideoPrefetcher {
|
class VideoPrefetcher {
|
||||||
private prefetchQueue: Set<string> = new Set();
|
private prefetchQueue: Set<string> = new Set();
|
||||||
private config: PrefetchConfig;
|
private config: PrefetchConfig;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
|
||||||
constructor(config: Partial<PrefetchConfig> = {}) {
|
constructor(config: Partial<PrefetchConfig> = {}) {
|
||||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
await videoCache.init();
|
await videoCache.init();
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async prefetchNext(
|
async prefetchNext(
|
||||||
videos: Video[],
|
videos: Video[],
|
||||||
currentIndex: number
|
currentIndex: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.isInitialized) await this.init();
|
if (!this.isInitialized) await this.init();
|
||||||
|
|
||||||
const endIndex = Math.min(
|
const endIndex = Math.min(
|
||||||
currentIndex + this.config.lookahead,
|
currentIndex + this.config.lookahead,
|
||||||
videos.length
|
videos.length
|
||||||
);
|
);
|
||||||
|
|
||||||
const toPrefetch = videos
|
const toPrefetch = videos
|
||||||
.slice(currentIndex + 1, endIndex)
|
.slice(currentIndex + 1, endIndex)
|
||||||
.filter((v) => v.url && !this.prefetchQueue.has(v.id));
|
.filter((v) => v.url && !this.prefetchQueue.has(v.id));
|
||||||
|
|
||||||
for (const video of toPrefetch) {
|
for (const video of toPrefetch) {
|
||||||
this.prefetchQueue.add(video.id);
|
this.prefetchQueue.add(video.id);
|
||||||
this.prefetchVideo(video).catch(console.error);
|
this.prefetchVideo(video).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefetch initial batch of videos immediately after feed loads.
|
* Prefetch initial batch of videos immediately after feed loads.
|
||||||
* This ensures first few videos are ready before user starts scrolling.
|
* This ensures first few videos are ready before user starts scrolling.
|
||||||
*/
|
*/
|
||||||
async prefetchInitialBatch(
|
async prefetchInitialBatch(
|
||||||
videos: Video[],
|
videos: Video[],
|
||||||
count: number = 3
|
count: number = 3
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.isInitialized) await this.init();
|
if (!this.isInitialized) await this.init();
|
||||||
if (videos.length === 0) return;
|
if (videos.length === 0) return;
|
||||||
|
|
||||||
console.log(`PREFETCH: Starting initial batch of ${Math.min(count, videos.length)} videos...`);
|
console.log(`PREFETCH: Starting initial batch of ${Math.min(count, videos.length)} videos...`);
|
||||||
|
|
||||||
const toPrefetch = videos
|
const toPrefetch = videos
|
||||||
.slice(0, count)
|
.slice(0, count)
|
||||||
.filter((v) => v.url && !this.prefetchQueue.has(v.id));
|
.filter((v) => v.url && !this.prefetchQueue.has(v.id));
|
||||||
|
|
||||||
// Start all prefetches in parallel (respects concurrency via browser limits)
|
// Start all prefetches in parallel (respects concurrency via browser limits)
|
||||||
const promises = toPrefetch.map((video) => {
|
const promises = toPrefetch.map((video) => {
|
||||||
this.prefetchQueue.add(video.id);
|
this.prefetchQueue.add(video.id);
|
||||||
return this.prefetchVideo(video);
|
return this.prefetchVideo(video);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
console.log(`PREFETCH: Initial batch complete (${toPrefetch.length} videos buffered)`);
|
console.log(`PREFETCH: Initial batch complete (${toPrefetch.length} videos buffered)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async prefetchVideo(video: Video): Promise<void> {
|
private async prefetchVideo(video: Video): Promise<void> {
|
||||||
if (!video.url) return;
|
if (!video.url) return;
|
||||||
|
|
||||||
const cached = await videoCache.get(video.url);
|
const cached = await videoCache.get(video.url);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.prefetchQueue.delete(video.id);
|
this.prefetchQueue.delete(video.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:8002/api'; // Hardcoded or imported from config
|
const API_BASE_URL = 'http://localhost:8002/api'; // Hardcoded or imported from config
|
||||||
const fullProxyUrl = `${API_BASE_URL}/feed/proxy?url=${encodeURIComponent(video.url)}`;
|
const fullProxyUrl = `${API_BASE_URL}/feed/proxy?url=${encodeURIComponent(video.url)}`;
|
||||||
// Use thin proxy if available for better performance
|
// Use thin proxy if available for better performance
|
||||||
const thinProxyUrl = video.cdn_url ? `${API_BASE_URL}/feed/thin-proxy?cdn_url=${encodeURIComponent(video.cdn_url)}` : null;
|
const thinProxyUrl = video.cdn_url ? `${API_BASE_URL}/feed/thin-proxy?cdn_url=${encodeURIComponent(video.cdn_url)}` : null;
|
||||||
const targetUrl = thinProxyUrl || fullProxyUrl;
|
const targetUrl = thinProxyUrl || fullProxyUrl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(
|
const timeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
this.config.timeoutMs
|
this.config.timeoutMs
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await fetch(targetUrl, {
|
const response = await fetch(targetUrl, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: { Range: 'bytes=0-1048576' }
|
headers: { Range: 'bytes=0-1048576' }
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
await videoCache.set(video.url, blob);
|
await videoCache.set(video.url, blob);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug(`Prefetch failed for ${video.id}:`, error);
|
console.debug(`Prefetch failed for ${video.id}:`, error);
|
||||||
} finally {
|
} finally {
|
||||||
this.prefetchQueue.delete(video.id);
|
this.prefetchQueue.delete(video.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearQueue(): void {
|
clearQueue(): void {
|
||||||
this.prefetchQueue.clear();
|
this.prefetchQueue.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueueSize(): number {
|
getQueueSize(): number {
|
||||||
return this.prefetchQueue.size;
|
return this.prefetchQueue.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const videoPrefetcher = new VideoPrefetcher();
|
export const videoPrefetcher = new VideoPrefetcher();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("Testing /health...")
|
print("Testing /health...")
|
||||||
with urllib.request.urlopen("http://localhost:8002/health", timeout=5) as r:
|
with urllib.request.urlopen("http://localhost:8002/health", timeout=5) as r:
|
||||||
print(f"Health: {r.status}")
|
print(f"Health: {r.status}")
|
||||||
|
|
||||||
print("Testing /api/feed...")
|
print("Testing /api/feed...")
|
||||||
with open("temp_cookies.json", "r") as f:
|
with open("temp_cookies.json", "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Ensure list format
|
# Ensure list format
|
||||||
if isinstance(data, dict) and "credentials" in data:
|
if isinstance(data, dict) and "credentials" in data:
|
||||||
data = data["credentials"]
|
data = data["credentials"]
|
||||||
|
|
||||||
# Prepare body as dict for safety with new Union type
|
# Prepare body as dict for safety with new Union type
|
||||||
body = {"credentials": data}
|
body = {"credentials": data}
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
"http://localhost:8002/api/feed",
|
"http://localhost:8002/api/feed",
|
||||||
data=json.dumps(body).encode('utf-8'),
|
data=json.dumps(body).encode('utf-8'),
|
||||||
headers={'Content-Type': 'application/json'}
|
headers={'Content-Type': 'application/json'}
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req, timeout=30) as r:
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
print(f"Feed: {r.status}")
|
print(f"Feed: {r.status}")
|
||||||
print(r.read().decode('utf-8')[:100])
|
print(r.read().decode('utf-8')[:100])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1,314 +1,314 @@
|
||||||
{
|
{
|
||||||
"credentials": [
|
"credentials": [
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1784039026,
|
"expirationDate": 1784039026,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "delay_guest_mode_vid",
|
"name": "delay_guest_mode_vid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "8"
|
"value": "8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1768131143.251207,
|
"expirationDate": 1768131143.251207,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "msToken",
|
"name": "msToken",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "jntmMFSrdBzHw3GQQ7xigi2HLM03wLgd2s8xW8sa8bm3gVg-VJu63FlYSfvPAW6tmoNM-Ww9ho9sOKZc75EN1XIGwct0ndkyOairFWbXgkiFwPXfDpQaBA9pn2_9mSOYSylT1H60yH1ufg=="
|
"value": "jntmMFSrdBzHw3GQQ7xigi2HLM03wLgd2s8xW8sa8bm3gVg-VJu63FlYSfvPAW6tmoNM-Ww9ho9sOKZc75EN1XIGwct0ndkyOairFWbXgkiFwPXfDpQaBA9pn2_9mSOYSylT1H60yH1ufg=="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646103,
|
"expirationDate": 1782817620.646103,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_session_tlb_tag",
|
"name": "tt_session_tlb_tag",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "sttt%7C1%7C6M35zM57kkqAGSs_LSUdRP_________zyplxYaEARSr2PNU_6cKcB0lq4WRz1GKKY43u399i5hs%3D"
|
"value": "sttt%7C1%7C6M35zM57kkqAGSs_LSUdRP_________zyplxYaEARSr2PNU_6cKcB0lq4WRz1GKKY43u399i5hs%3D"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1798369620.645922,
|
"expirationDate": 1798369620.645922,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_guard",
|
"name": "sid_guard",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44%7C1767265616%7C15552000%7CTue%2C+30-Jun-2026+11%3A06%3A56+GMT"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44%7C1767265616%7C15552000%7CTue%2C+30-Jun-2026+11%3A06%3A56+GMT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1798801628.385793,
|
"expirationDate": 1798801628.385793,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "ttwid",
|
"name": "ttwid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1%7CAYDsetgnxt5vzYX8hD6Wq2DQ4FXiL_pqcdLwHWwz6B8%7C1767265624%7C6121d82381fb651afeae94341e45b87fca1d903fbec0d8a19e4dd5440a89a424"
|
"value": "1%7CAYDsetgnxt5vzYX8hD6Wq2DQ4FXiL_pqcdLwHWwz6B8%7C1767265624%7C6121d82381fb651afeae94341e45b87fca1d903fbec0d8a19e4dd5440a89a424"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1767870430,
|
"expirationDate": 1767870430,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "perf_feed_cache",
|
"name": "perf_feed_cache",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "{%22expireTimestamp%22:1767438000000%2C%22itemIds%22:[%227588749061168123154%22%2C%227589493510613552404%22%2C%227586917939568332054%22]}"
|
"value": "{%22expireTimestamp%22:1767438000000%2C%22itemIds%22:[%227588749061168123154%22%2C%227589493510613552404%22%2C%227586917939568332054%22]}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.645952,
|
"expirationDate": 1782817620.645952,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "uid_tt",
|
"name": "uid_tt",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449601.742227,
|
"expirationDate": 1772449601.742227,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "passport_csrf_token_default",
|
"name": "passport_csrf_token_default",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "s_v_web_id",
|
"name": "s_v_web_id",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": true,
|
"session": true,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "verify_mjvcbi31_l3mxUEeU_ykis_4x6z_859S_8zEFmNseEnJU"
|
"value": "verify_mjvcbi31_l3mxUEeU_ykis_4x6z_859S_8zEFmNseEnJU"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.64617,
|
"expirationDate": 1782817620.64617,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "ssid_ucp_v1",
|
"name": "ssid_ucp_v1",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1793185625,
|
"expirationDate": 1793185625,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "tiktok_webapp_theme",
|
"name": "tiktok_webapp_theme",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "dark"
|
"value": "dark"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1799409936.219767,
|
"expirationDate": 1799409936.219767,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "_ttp",
|
"name": "_ttp",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "32XOXKxwj8YLtBQf0OBn4TvlkPN"
|
"value": "32XOXKxwj8YLtBQf0OBn4TvlkPN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449620.645821,
|
"expirationDate": 1772449620.645821,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "cmpl_token",
|
"name": "cmpl_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "AgQYAPOF_hfkTtKPtFExgPKdOPKrXVkNUj-FDmCi6K4"
|
"value": "AgQYAPOF_hfkTtKPtFExgPKdOPKrXVkNUj-FDmCi6K4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449620.645628,
|
"expirationDate": 1772449620.645628,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "multi_sids",
|
"name": "multi_sids",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "6884525631502042113%3Ae8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "6884525631502042113%3Ae8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1769857620.645892,
|
"expirationDate": 1769857620.645892,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "passport_auth_status_ss",
|
"name": "passport_auth_status_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "966972581a398dbb9ead189c044cf98c%2C"
|
"value": "966972581a398dbb9ead189c044cf98c%2C"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1772449601.742082,
|
"expirationDate": 1772449601.742082,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "passport_csrf_token",
|
"name": "passport_csrf_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
"value": "9c66ab10306611c75fa19c87e54fd31b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646041,
|
"expirationDate": 1782817620.646041,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sessionid",
|
"name": "sessionid",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646073,
|
"expirationDate": 1782817620.646073,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sessionid_ss",
|
"name": "sessionid_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646008,
|
"expirationDate": 1782817620.646008,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_tt",
|
"name": "sid_tt",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
"value": "e8cdf9ccce7b924a80192b3f2d251d44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.646132,
|
"expirationDate": 1782817620.646132,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "sid_ucp_v1",
|
"name": "sid_ucp_v1",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
"value": "1.0.1-KDlhMTg2NTQ1MmJiZmNlZjgzYzNiZGU4ZjAyNzk1NWRkNTlkOTYxNjIKIQiBiIHG4PKvxV8Q0KrZygYYswsgDDC50ZfDBjgIQBJIBBADGgJteSIgZThjZGY5Y2NjZTdiOTI0YTgwMTkyYjNmMmQyNTFkNDQyTgog40q2JTBb3lGgiNKowpX3zbxplmW4zO3AUFhAo6LMB-wSIDpAp_OQ2Q5qEBZvL59v7fgLmw27UIxLQHoimzDg3U5BGAIiBnRpa3Rvaw"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".www.tiktok.com",
|
"domain": ".www.tiktok.com",
|
||||||
"expirationDate": 1793185625,
|
"expirationDate": 1793185625,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": false,
|
"httpOnly": false,
|
||||||
"name": "tiktok_webapp_theme_source",
|
"name": "tiktok_webapp_theme_source",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "auto"
|
"value": "auto"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817624.001151,
|
"expirationDate": 1782817624.001151,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_chain_token",
|
"name": "tt_chain_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": null,
|
"sameSite": null,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "6deMEWrkAGUe9R0tCISIoQ=="
|
"value": "6deMEWrkAGUe9R0tCISIoQ=="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "tt_csrf_token",
|
"name": "tt_csrf_token",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "lax",
|
"sameSite": "lax",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": true,
|
"session": true,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "q0Q4ki72-I7zQB6eLbpBBaqFBGrUF_v85N9s"
|
"value": "q0Q4ki72-I7zQB6eLbpBBaqFBGrUF_v85N9s"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"domain": ".tiktok.com",
|
"domain": ".tiktok.com",
|
||||||
"expirationDate": 1782817620.645979,
|
"expirationDate": 1782817620.645979,
|
||||||
"hostOnly": false,
|
"hostOnly": false,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"name": "uid_tt_ss",
|
"name": "uid_tt_ss",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"sameSite": "no_restriction",
|
"sameSite": "no_restriction",
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"session": false,
|
"session": false,
|
||||||
"storeId": null,
|
"storeId": null,
|
||||||
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
"value": "44deb1e89d254f610eefd18c39ec97fa708e9c0f22c0207f7140c6ffd6c81b2c"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
|
|
||||||
URL = "http://localhost:8002/api/auth/admin-login"
|
URL = "http://localhost:8002/api/auth/admin-login"
|
||||||
|
|
||||||
def test_login():
|
def test_login():
|
||||||
print("Testing Admin Login...")
|
print("Testing Admin Login...")
|
||||||
try:
|
try:
|
||||||
res = requests.post(URL, json={"password": "admin123"})
|
res = requests.post(URL, json={"password": "admin123"})
|
||||||
print(f"Status: {res.status_code}")
|
print(f"Status: {res.status_code}")
|
||||||
print(f"Response: {res.text}")
|
print(f"Response: {res.text}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_login()
|
test_login()
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
with open("temp_cookies.json", "r") as f:
|
with open("temp_cookies.json", "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Ensure data is in the expected dict format for the request body
|
# Ensure data is in the expected dict format for the request body
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
# If temp_cookies is just the list, wrap it
|
# If temp_cookies is just the list, wrap it
|
||||||
body = {"credentials": data}
|
body = {"credentials": data}
|
||||||
elif "credentials" not in data:
|
elif "credentials" not in data:
|
||||||
body = {"credentials": data}
|
body = {"credentials": data}
|
||||||
else:
|
else:
|
||||||
body = data
|
body = data
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
"http://localhost:8002/api/feed",
|
"http://localhost:8002/api/feed",
|
||||||
data=json.dumps(body).encode('utf-8'),
|
data=json.dumps(body).encode('utf-8'),
|
||||||
headers={'Content-Type': 'application/json'}
|
headers={'Content-Type': 'application/json'}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req) as response:
|
with urllib.request.urlopen(req) as response:
|
||||||
print(response.read().decode('utf-8'))
|
print(response.read().decode('utf-8'))
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(f"HTTP Error: {e.code}")
|
print(f"HTTP Error: {e.code}")
|
||||||
print(e.read().decode('utf-8'))
|
print(e.read().decode('utf-8'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
BASE_URL = "http://localhost:8002/api/user/search"
|
BASE_URL = "http://localhost:8002/api/user/search"
|
||||||
|
|
||||||
def test_search():
|
def test_search():
|
||||||
print("Testing Search API...")
|
print("Testing Search API...")
|
||||||
try:
|
try:
|
||||||
# Simple query
|
# Simple query
|
||||||
params = {
|
params = {
|
||||||
"query": "dance",
|
"query": "dance",
|
||||||
"limit": 50,
|
"limit": 50,
|
||||||
"cursor": 0
|
"cursor": 0
|
||||||
}
|
}
|
||||||
start = time.time()
|
start = time.time()
|
||||||
res = requests.get(BASE_URL, params=params)
|
res = requests.get(BASE_URL, params=params)
|
||||||
duration = time.time() - start
|
duration = time.time() - start
|
||||||
|
|
||||||
print(f"Status Code: {res.status_code}")
|
print(f"Status Code: {res.status_code}")
|
||||||
print(f"Duration: {duration:.2f}s")
|
print(f"Duration: {duration:.2f}s")
|
||||||
|
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
data = res.json()
|
data = res.json()
|
||||||
print(f"Videos Found: {len(data.get('videos', []))}")
|
print(f"Videos Found: {len(data.get('videos', []))}")
|
||||||
# print(json.dumps(data, indent=2))
|
# print(json.dumps(data, indent=2))
|
||||||
else:
|
else:
|
||||||
print("Error Response:")
|
print("Error Response:")
|
||||||
print(res.text)
|
print(res.text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Request Failed: {e}")
|
print(f"Request Failed: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test_search()
|
test_search()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue