diff --git a/Dockerfile b/Dockerfile index dd6a7de..bb1c69c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,14 +7,7 @@ COPY frontend/ ./ RUN npm run build # Runtime Stage for Backend -FROM python:3.11-slim - -# Install system dependencies required for Playwright and compiled extensions -RUN apt-get update && apt-get install -y \ - curl \ - git \ - build-essential \ - && rm -rf /var/lib/apt/lists/* +FROM mcr.microsoft.com/playwright/python:v1.49.1-jammy WORKDIR /app @@ -22,10 +15,6 @@ WORKDIR /app COPY backend/requirements.txt backend/ RUN pip install --no-cache-dir -r backend/requirements.txt -# Install Playwright browsers (Chromium only to save space) -RUN playwright install chromium -RUN playwright install-deps chromium - # Copy Backend Code COPY backend/ backend/ diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..3807756 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,32 @@ +# PureStream Development Dockerfile +# Copies all files to avoid Synology Drive volume mount issues + +FROM mcr.microsoft.com/playwright/python:v1.49.1-jammy + +WORKDIR /app + +# Copy backend files +COPY backend/ /app/backend/ + +# Copy pre-built frontend +COPY frontend/dist/ /app/frontend/dist/ + +# Create directories for cache and session +RUN mkdir -p /app/cache /app/session + +# Install Python dependencies +WORKDIR /app/backend +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install playwright-stealth && \ + playwright install chromium + +# Environment variables +ENV PYTHONUNBUFFERED=1 +ENV CACHE_DIR=/app/cache +ENV MAX_CACHE_SIZE_MB=500 +ENV CACHE_TTL_HOURS=24 +ENV ADMIN_PASSWORD=admin123 + +EXPOSE 8002 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/backend/api/routes/feed.py b/backend/api/routes/feed.py index ee7471d..b8b9e69 100644 --- a/backend/api/routes/feed.py +++ b/backend/api/routes/feed.py @@ -134,6 +134,77 @@ init_cache() from typing import Optional, Any, Union, List, Dict +# ========== FEED METADATA CACHE ========== +FEED_METADATA_CACHE = os.path.join(tempfile.gettempdir(), "feed_metadata.json") +METADATA_TTL_HOURS = 24 # Keep feed order for 24 hours (for instant load) + +def load_cached_feed() -> Optional[List[dict]]: + """Load cached feed metadata for instant startup.""" + if not os.path.exists(FEED_METADATA_CACHE): + return None + + try: + if (time.time() - os.path.getmtime(FEED_METADATA_CACHE)) > (METADATA_TTL_HOURS * 3600): + return None + + with open(FEED_METADATA_CACHE, "r") as f: + return json.load(f) + except: + return None + +def save_cached_feed(videos: List[dict]): + """Save feed metadata to cache.""" + try: + with open(FEED_METADATA_CACHE, "w") as f: + json.dump(videos, f) + except Exception as e: + print(f"DEBUG: Failed to save feed metadata: {e}") + +# Import services for fallback aggregation +from core.tiktok_api_service import TikTokAPIService +from api.routes.user import get_fallback_accounts + +async def generate_fallback_feed(limit: int = 5) -> List[dict]: + """ + Generate a feed by aggregating latest videos from verified creators. + Used when cache is empty and we want to avoid Playwright startup headers. + """ + print("DEBUG: Generating fallback feed from verified users...") + cookies, user_agent = PlaywrightManager.load_stored_credentials() + + # Use verified accounts from fallback list + accounts = get_fallback_accounts() + # Shuffle accounts to get variety + import random + random.shuffle(accounts) + + # Select top 5-8 accounts to query + selected = accounts[:8] + + tasks = [] + # We only need 1 recent video from each to make a "feed" + for acc in selected: + tasks.append(TikTokAPIService.get_user_videos( + acc['username'], + cookies=cookies, + user_agent=user_agent, + limit=1 # Just 1 video per creator + )) + + # Run in parallel + results = await asyncio.gather(*tasks, return_exceptions=True) + + feed = [] + for res in results: + if isinstance(res, list) and res: + feed.extend(res) + + # Shuffle the resulting feed + random.shuffle(feed) + + print(f"DEBUG: Generated fallback feed with {len(feed)} videos") + return feed[:limit] + class FeedRequest(BaseModel): """Request body for feed endpoint with optional JSON credentials.""" credentials: Optional[Union[Dict, List]] = None @@ -171,12 +242,39 @@ async def get_feed_simple(fast: bool = False, skip_cache: bool = False): # When skipping cache for infinite scroll, do more scrolling to get different videos if skip_cache: - scroll_count = 8 # More scrolling to get fresh content + scroll_count = 8 + # [OPTIMIZATION] Fast Load Strategy + if fast and not skip_cache: + # 1. Try Memory/Disk Cache first (Instant) + cached_feed = load_cached_feed() + if cached_feed: + print(f"DEBUG: Returning cached feed ({len(cached_feed)} videos)") + return cached_feed + + # 2. Try Fallback Aggregation (Fast HTTP, no browser) + # This fetches real latest videos from top creators via direct API + try: + aggregated = await generate_fallback_feed(limit=5) + if aggregated: + save_cached_feed(aggregated) + return aggregated + except Exception as agg_err: + print(f"DEBUG: Aggregation fallback failed: {agg_err}") + + # 3. Playwright Interception (Slowest, but guaranteed 'For You' algorithm) videos = await PlaywrightManager.intercept_feed(scroll_count=scroll_count) + + # Save successful result to cache for next time + if videos and len(videos) > 0 and not skip_cache: + save_cached_feed(videos) + return videos except Exception as e: print(f"DEBUG: Feed error: {e}") + + # 4. Ultimate Fallback if everything fails (Verified users static list?) + # For now just re-raise, as UI handles empty state raise HTTPException(status_code=500, detail=str(e)) @@ -281,21 +379,29 @@ async def proxy_video( if not os.path.exists(video_path): raise Exception("Video file not created") - print(f"Downloaded codec: {video_codec} (no transcoding - client will decode)") + print(f"Downloaded codec: {video_codec}") - # Save to cache directly - NO TRANSCODING + # Save to cache directly - client-side player handles all formats cached_path = save_to_cache(url, video_path) stats = get_cache_stats() print(f"CACHED: {url[:50]}... ({stats['files']} files, {stats['size_mb']}MB total)") except Exception as e: - print(f"DEBUG: yt-dlp download failed: {e}") + print(f"DEBUG: yt-dlp download failed: {str(e)}") # Cleanup if cookie_file_path and os.path.exists(cookie_file_path): - os.unlink(cookie_file_path) + try: + os.unlink(cookie_file_path) + except: + pass if os.path.exists(temp_dir): - shutil.rmtree(temp_dir, ignore_errors=True) - raise HTTPException(status_code=500, detail=f"Could not download video: {e}") + try: + shutil.rmtree(temp_dir, ignore_errors=True) + except: + pass + + # Return 422 for processing failure instead of 500 (server crash) + raise HTTPException(status_code=422, detail=f"Video processing failed: {str(e)}") # Cleanup temp (cached file is separate) if cookie_file_path and os.path.exists(cookie_file_path): @@ -393,4 +499,9 @@ async def thin_proxy_video( except Exception as e: print(f"Thin proxy error: {e}") # Ensure cleanup if possible - raise HTTPException(status_code=500, detail=str(e)) + if 'r' in locals(): + await r.aclose() + if 'client' in locals(): + await client.aclose() + + raise HTTPException(status_code=502, detail=f"Upstream Proxy Error: {str(e)}") diff --git a/backend/api/routes/user.py b/backend/api/routes/user.py index 7a9d545..97a8fee 100644 --- a/backend/api/routes/user.py +++ b/backend/api/routes/user.py @@ -7,8 +7,12 @@ from pydantic import BaseModel from typing import Optional, List import httpx import asyncio +import time +import re +from typing import Optional, List from core.playwright_manager import PlaywrightManager +from core.tiktok_api_service import TikTokAPIService router = APIRouter() @@ -112,7 +116,7 @@ async def get_user_videos( ): """ Fetch videos from a TikTok user's profile. - Uses Playwright to crawl the user's page for reliable results. + Uses direct API calls for speed (~100-500ms), with Playwright fallback. """ username = username.replace("@", "") @@ -123,10 +127,25 @@ async def get_user_videos( raise HTTPException(status_code=401, detail="Not authenticated") print(f"Fetching videos for @{username}...") + start_time = time.time() + # Try fast API first + try: + videos = await TikTokAPIService.get_user_videos(username, cookies, user_agent, limit) + if videos: + duration = time.time() - start_time + print(f"[API] Got {len(videos)} videos in {duration:.2f}s") + return {"username": username, "videos": videos, "count": len(videos), "source": "api", "duration_ms": int(duration * 1000)} + except Exception as e: + print(f"[API] Failed for {username}: {e}") + + # Fallback to Playwright if API fails or returns empty + print(f"[Fallback] Using Playwright for @{username}...") try: videos = await PlaywrightManager.fetch_user_videos(username, cookies, user_agent, limit) - return {"username": username, "videos": videos, "count": len(videos)} + duration = time.time() - start_time + print(f"[Playwright] Got {len(videos)} videos in {duration:.2f}s") + return {"username": username, "videos": videos, "count": len(videos), "source": "playwright", "duration_ms": int(duration * 1000)} except Exception as e: print(f"Error fetching videos for {username}: {e}") raise HTTPException(status_code=500, detail=str(e)) @@ -140,7 +159,7 @@ async def search_videos( ): """ Search for videos by keyword or hashtag. - Uses Playwright to crawl TikTok search results for reliable data. + Uses direct API calls for speed (~200-800ms), with Playwright fallback. """ # Load stored credentials cookies, user_agent = PlaywrightManager.load_stored_credentials() @@ -149,13 +168,157 @@ async def search_videos( raise HTTPException(status_code=401, detail="Not authenticated") print(f"Searching for: {query} (limit={limit}, cursor={cursor})...") + start_time = time.time() + # [Smart Search] Username Detection Priority + # If query looks like a username (contains dots, underscores, or starts with @), + # try fetching that specific user's videos FIRST. + # This solves issues where searching for exact username returns unrelated content. + clean_query = query.strip() + + # Handle @ prefix commonly used by users - STRICT MODE + # If user explicitly types "@", they want a user lookup, NOT a keyword search. + strict_user_lookup = False + if clean_query.startswith("@"): + clean_query = clean_query[1:] + strict_user_lookup = True + + # Also treat dots/underscores as likely usernames + is_username_format = bool(re.match(r"^[a-zA-Z0-9_\.]+$", clean_query)) and len(clean_query) > 2 + + # DEBUG LOGGING TO FILE + try: + with open("search_debug.log", "a", encoding="utf-8") as f: + f.write(f"\n--- Search: {query} ---\n") + f.write(f"Strict: {strict_user_lookup}, Format: {is_username_format}, Clean: {clean_query}\n") + except: pass + + if is_username_format or strict_user_lookup: + print(f"[Smart Search] Query '{query}' identified as username. Strict: {strict_user_lookup}") + try: + # Try direct profile fetch via API + videos = await TikTokAPIService.get_user_videos(clean_query, cookies, user_agent, limit) + if videos: + duration = time.time() - start_time + print(f"[API-Profile-Priority] Found {len(videos)} videos for user '{query}' in {duration:.2f}s") + return {"query": query, "videos": videos, "count": len(videos), "cursor": 0, "source": "user_profile_priority", "duration_ms": int(duration * 1000)} + + # Try Playwright fallback BEFORE yt-dlp + # Playwright scraping provides thumbnails and correct metadata, while yt-dlp flat-playlist does not. + print(f"[Smart Search] API failed, trying Playwright for user '{query}'...") + try: + videos = await PlaywrightManager.fetch_user_videos(clean_query, cookies, user_agent, limit) + if videos: + duration = time.time() - start_time + print(f"[Playwright-Profile-Priority] Found {len(videos)} videos for user '{query}' in {duration:.2f}s") + return {"query": query, "videos": videos, "count": len(videos), "cursor": 0, "source": "user_profile_playwright_priority", "duration_ms": int(duration * 1000)} + except Exception as pw_err: + print(f"[Smart Search] Playwright profile fetch failed: {pw_err}") + + # Try yt-dlp fallback if Playwright also fails + print(f"[Smart Search] Playwright failed, trying yt-dlp for user '{query}'...") + + # Log we are trying ytdlp + try: + with open("search_debug.log", "a", encoding="utf-8") as f: + f.write(f"Attempting yt-dlp for {clean_query}...\n") + except: pass + + videos = await TikTokAPIService.get_user_videos_via_ytdlp(clean_query, limit) + + try: + with open("search_debug.log", "a", encoding="utf-8") as f: + f.write(f"yt-dlp Result: {len(videos)} videos\n") + except: pass + + if videos: + duration = time.time() - start_time + print(f"[yt-dlp-Priority] Found {len(videos)} videos for user '{query}' in {duration:.2f}s") + return {"query": query, "videos": videos, "count": len(videos), "cursor": 0, "source": "user_profile_ytdlp", "duration_ms": int(duration * 1000)} + + # If strict usage of "@" was used and we found nothing, DO NOT fallback to generic search. + # It's better to show "No videos found" than random unrelated results. + if strict_user_lookup: + print(f"[Smart Search] Strict lookup for '{query}' found no results. Returning empty.") + return {"query": query, "videos": [], "count": 0, "cursor": 0, "source": "user_not_found_strict", "duration_ms": int((time.time() - start_time) * 1000)} + + + except Exception as e: + print(f"[Smart Search] Priority profile fetch failed: {e}") + if strict_user_lookup: + return {"query": query, "videos": [], "count": 0, "cursor": 0, "source": "error_strict", "duration_ms": int((time.time() - start_time) * 1000)} + # Fall through to normal search only if NOT strict + + # Try fast API search + try: + videos = await TikTokAPIService.search_videos(query, cookies, user_agent, limit, cursor) + if videos: + duration = time.time() - start_time + print(f"[API] Found {len(videos)} videos in {duration:.2f}s") + return {"query": query, "videos": videos, "count": len(videos), "cursor": cursor + len(videos), "source": "api", "duration_ms": int(duration * 1000)} + except Exception as e: + print(f"[API] Search failed for {query}: {e}") + + # Fallback Phase 1: Check if query is a "trending" misspelling and retry API if so + # Regex for: hot, trend, trens, hor, hott, trand, etc. + trend_pattern = r"(hot|hor|hott)\s*(trend|trens|trand|tred)|(trend|trens|trand)" + is_trend_query = bool(re.search(trend_pattern, query.lower())) + + if is_trend_query and (not videos): + print(f"[Smart Fallback] Query '{query}' detected as trending request. Retrying with 'hot trend'...") + try: + # Try normalized query on API + videos = await TikTokAPIService.search_videos("hot trend", cookies, user_agent, limit, cursor) + if videos: + duration = time.time() - start_time + print(f"[API-Fallback] Found {len(videos)} videos for 'hot trend' in {duration:.2f}s") + return {"query": "hot trend", "videos": videos, "count": len(videos), "cursor": cursor + len(videos), "source": "api_fallback", "duration_ms": int(duration * 1000)} + except Exception: + pass # Continue to Playwright if this fails + + # Fallback Phase 2: Playwright + # Fallback to Playwright if API fails or returns empty + print(f"[Fallback] Using Playwright for search '{query}'...") try: videos = await PlaywrightManager.search_videos(query, cookies, user_agent, limit, cursor) - return {"query": query, "videos": videos, "count": len(videos), "cursor": cursor + len(videos)} + + # Smart Fallback Phase 3: If Playwright also fails for trending query, try normalized query + if not videos and is_trend_query: + print(f"[Playwright-Fallback] No results for '{query}'. Retrying with 'hot trend'...") + videos = await PlaywrightManager.search_videos("hot trend", cookies, user_agent, limit, cursor) + except Exception as e: print(f"Error searching for {query}: {e}") - raise HTTPException(status_code=500, detail=str(e)) + # Don't raise yet, try user fallback + pass + + # Fallback Phase 4: Exact Username Match (Secondary Fallback) + # If generic search failed, and query looks like a username, try fetching their profile directly (if not tried already) + # Note: We already tried this at the top, but we try again here with Playwright as a backup if the API profile fetch failed earlier. + + if (not videos) and is_username_format: + print(f"[Smart Fallback] Query '{query}' yielded no search results. Attempting secondary profile fetch (Playwright)...") + try: + # We already tried API profile fetch at start, so try Playwright now + print(f"[Smart Fallback] API failed, trying Playwright for user '{query}'...") + videos = await PlaywrightManager.fetch_user_videos(query, cookies, user_agent, limit) + if videos: + duration = time.time() - start_time + print(f"[Playwright-Profile] Found {len(videos)} videos for user '{query}' in {duration:.2f}s") + return {"query": query, "videos": videos, "count": len(videos), "cursor": 0, "source": "user_profile_playwright", "duration_ms": int(duration * 1000)} + + except Exception as e: + print(f"[Smart Fallback] Profile fetch failed: {e}") + pass + + if not videos: + # Only raise error if we truly found nothing after all attempts + # or return empty list instead of 500? + # A 500 implies server broken. Empty list implies no results. + # Let's return empty structure to be safe for frontend + return {"query": query, "videos": [], "count": 0, "cursor": cursor, "source": "empty"} + + return {"query": query, "videos": videos, "count": len(videos), "cursor": cursor + len(videos), "source": "playwright", "duration_ms": int((time.time() - start_time) * 1000)} # Cache for suggested accounts @@ -178,7 +341,6 @@ async def get_suggested_accounts( # Check cache if _suggested_cache["accounts"] and (time.time() - _suggested_cache["updated_at"]) < CACHE_TTL: - print("Returning cached suggested accounts") return {"accounts": _suggested_cache["accounts"][:limit], "cached": True} # Load stored credentials @@ -191,17 +353,24 @@ async def get_suggested_accounts( print("Fetching fresh suggested accounts from TikTok...") try: - accounts = await PlaywrightManager.fetch_suggested_accounts(cookies, user_agent, limit) + # Enforce a strict timeout to prevent hanging or heavy resource usage blocking the server + # If Playwright takes > 15 seconds, we default to fallback. + try: + accounts = await asyncio.wait_for( + PlaywrightManager.fetch_suggested_accounts(cookies, user_agent, limit), + timeout=15.0 + ) + except asyncio.TimeoutError: + print("Suggest fetch timed out, using fallback.") + accounts = [] if accounts and len(accounts) >= 5: # Need at least 5 accounts from dynamic fetch _suggested_cache["accounts"] = accounts _suggested_cache["updated_at"] = time.time() return {"accounts": accounts[:limit], "cached": False} else: - # Fallback: fetch actual profile data with avatars for static list - print("Dynamic fetch failed, fetching profile data for static accounts...") - fallback_list = get_fallback_accounts()[:min(limit, 20)] # Limit to 20 for speed - return await fetch_profiles_with_avatars(fallback_list, cookies, user_agent) + # Just return static accounts directly without API calls - TikTok API is unreliable + return {"accounts": get_fallback_accounts()[:limit], "cached": False, "fallback": True} except Exception as e: print(f"Error fetching suggested accounts: {e}") diff --git a/backend/core/download_service.py b/backend/core/download_service.py index 7957c3f..a9c3f68 100644 --- a/backend/core/download_service.py +++ b/backend/core/download_service.py @@ -7,6 +7,23 @@ class DownloadService: self.download_dir = "downloads" if not os.path.exists(self.download_dir): os.makedirs(self.download_dir) + + # Auto-update yt-dlp on startup (Disabled for stability/speed) + # self.update_ytdlp() + + def update_ytdlp(self): + """ + Auto-update yt-dlp to the latest nightly/pre-release version. + """ + try: + print("Checking for yt-dlp updates (nightly)...") + import subprocess + import sys + # Use the current python executable to run pip + subprocess.check_call([sys.executable, "-m", "pip", "install", "-U", "--pre", "yt-dlp", "--break-system-packages"]) + print("yt-dlp updated successfully.") + except Exception as e: + print(f"Failed to update yt-dlp: {e}") async def download_video(self, url: str) -> dict: """ diff --git a/backend/core/playwright_manager.py b/backend/core/playwright_manager.py index 0e9fca9..ebc132a 100644 --- a/backend/core/playwright_manager.py +++ b/backend/core/playwright_manager.py @@ -17,9 +17,14 @@ from playwright.async_api import async_playwright, Response, Browser, BrowserCon try: from playwright_stealth import stealth_async except ImportError: - print("WARNING: playwright_stealth not found, disabling stealth mode.") - async def stealth_async(page): - pass + try: + from playwright_stealth import Stealth + async def stealth_async(page): + await Stealth().apply_stealth_async(page) + except ImportError: + print("WARNING: playwright_stealth not found, disabling stealth mode.") + async def stealth_async(page): + pass COOKIES_FILE = "cookies.json" @@ -43,10 +48,18 @@ class PlaywrightManager: "--start-maximized" ] - DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" # Use installed Chrome instead of Playwright's Chromium (avoids slow download) - CHROME_PATH = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + import platform + import os + + # Check if running on macOS + if platform.system() == "Darwin" and os.path.exists("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"): + CHROME_PATH = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + else: + # On Linux/Docker, use Playwright's bundled Chromium (None lets Playwright decide) + CHROME_PATH = None # VNC login state (class-level to persist across requests) _vnc_playwright = None @@ -515,8 +528,26 @@ class PlaywrightManager: try: data = await response.json() - # TikTok returns videos in "itemList" or "aweme_list" - items = data.get("itemList", []) or data.get("aweme_list", []) + # TikTok returns videos in various nested formats + items = [] + + # Try direct itemList first + if data.get("itemList") and isinstance(data["itemList"], list): + items = data["itemList"] + elif data.get("aweme_list") and isinstance(data["aweme_list"], list): + items = data["aweme_list"] + # Try nested data structure + elif data.get("data"): + nested_data = data["data"] + if isinstance(nested_data, list): + for item in nested_data: + if isinstance(item, dict): + if "item" in item and isinstance(item["item"], dict): + items.append(item["item"]) + else: + items.append(item) + elif isinstance(nested_data, dict): + items = nested_data.get("itemList", []) or nested_data.get("aweme_list", []) for item in items: video_data = PlaywrightManager._extract_video_data(item) @@ -744,10 +775,68 @@ class PlaywrightManager: # Wait for videos to load await asyncio.sleep(2) - # Scroll a bit to trigger more video loading - await page.evaluate("window.scrollBy(0, 500)") - await asyncio.sleep(1) + # Scroll loop to ensure we get enough videos + scroll_attempts = 0 + last_count = 0 + max_scrolls = 20 # Prevent infinite loops + while len(captured_videos) < limit and scroll_attempts < max_scrolls: + print(f"DEBUG: Scrolling profile (Current: {len(captured_videos)}/{limit})...") + await page.evaluate("window.scrollBy(0, 800)") + await asyncio.sleep(1.5) # Wait for network/DOM + + # DOM Fallback check inside loop (for hybrid loading) + if len(captured_videos) == last_count: + # If count didn't increase via network, try scraping DOM again + # This handles cases where TikTok renders new items in DOM without standard API + # (Unlikely for infinite scroll, but good safety) + pass + + last_count = len(captured_videos) + scroll_attempts += 1 + + # DOM Fallback: If no API captured (SSR case), scrape from DOM + if len(captured_videos) == 0: + print("DEBUG: No API response for user videos, trying DOM scrape (SSR)...") + video_elements = await page.locator('div[data-e2e="user-post-item"]').all() + + for el in video_elements: + if len(captured_videos) >= limit: + break + try: + # Extract data from DOM attributes/links + url = await el.locator("a").get_attribute("href") + desc = await el.locator("img").get_attribute("alt") + # Try to find specific img for cover + # Often the img alt is the description + + if url: + # Parse video ID and author from URL + # Format: https://www.tiktok.com/@user/video/123456... + if "/video/" in url: + parts = url.split("/video/") + vid_id = parts[1].split("?")[0] if len(parts) > 1 else "" + + # We already know the author from the function arg, but can verify + # Construct basic video object + dom_video = { + "id": vid_id, + "url": url, + "author": username, + "description": desc or f"Video by @{username}", + "views": 0, # Cannot easily get from list view DOM + "likes": 0 + } + + # Try to get thumbnail info + thumb = await el.locator("img").get_attribute("src") + if thumb: + dom_video["thumbnail"] = thumb + + captured_videos.append(dom_video) + except Exception as el_err: + print(f"DEBUG: Error extracting DOM item: {el_err}") + except Exception as e: print(f"DEBUG: Error navigating to profile: {e}") @@ -760,59 +849,19 @@ class PlaywrightManager: async def search_videos(query: str, cookies: list, user_agent: str = None, limit: int = 20, cursor: int = 0) -> list: """ Search for videos by keyword or hashtag. - Uses Playwright to intercept TikTok search results API. - - Args: - query: Search query - cookies: Auth cookies - user_agent: Browser user agent - limit: Max videos to capture in this batch - cursor: Starting offset for pagination + Optimized: Uses page.evaluate to fetch specific offsets via internal API. """ - from playwright.async_api import async_playwright, Response + from playwright.async_api import async_playwright from urllib.parse import quote + import json if not user_agent: user_agent = PlaywrightManager.DEFAULT_USER_AGENT - if not cookies: - print("DEBUG: No cookies available for search") - return [] - print(f"DEBUG: Searching for '{query}' (limit={limit}, cursor={cursor})...") captured_videos = [] - async def handle_response(response: Response): - """Capture search results API responses.""" - nonlocal captured_videos - - url = response.url - - # Look for search results API - if "search" in url and ("item_list" in url or "video" in url or "general" in url): - try: - data = await response.json() - - # Try different response formats - items = data.get("itemList", []) or data.get("data", []) or data.get("item_list", []) - - for item in items: - # If we have enough for this specific batch, we don't need more - if len(captured_videos) >= limit: - break - - video_data = PlaywrightManager._extract_video_data(item) - if video_data: - # Avoid duplicates within the same capture session - if not any(v['id'] == video_data['id'] for v in captured_videos): - captured_videos.append(video_data) - - print(f"DEBUG: Captured {len(items)} videos from search API (Total batch: {len(captured_videos)})") - - except Exception as e: - print(f"DEBUG: Error parsing search API response: {e}") - async with async_playwright() as p: browser = await p.chromium.launch( headless=True, @@ -820,51 +869,69 @@ class PlaywrightManager: args=PlaywrightManager.BROWSER_ARGS ) - context = await browser.new_context(user_agent=user_agent) - await context.add_cookies(cookies) + context = await browser.new_context( + user_agent=user_agent, + viewport={"width": 1280, "height": 720} + ) + + if cookies: + await context.add_cookies(cookies) page = await context.new_page() await stealth_async(page) - page.on("response", handle_response) try: - # Navigate to TikTok search page - search_url = f"https://www.tiktok.com/search/video?q={quote(query)}" - try: - await page.goto(search_url, wait_until="domcontentloaded", timeout=15000) - except: - print("DEBUG: Navigation timeout, proceeding anyway") + # 1. Navigate to Search Page to initialize session/state + # We don't need to wait for full load if we are just going to fetch + search_url = f"https://www.tiktok.com/search?q={quote(query)}" + await page.goto(search_url, wait_until="domcontentloaded", timeout=20000) - # Wait for initial results - await asyncio.sleep(3) + # 2. If cursor > 0 (or always), Try to fetch API directly from browser context + # This leverages the browser's valid session/signature generation + print(f"DEBUG: Executing internal API fetch for offset {cursor}...") - # Scroll based on cursor to reach previous results and then capture new ones - # Each scroll typically loads 12-20 items - # We scroll more as the cursor increases - scroll_count = (cursor // 10) + 1 - # Limit total scrolls to avoid hanging - scroll_count = min(scroll_count, 10) + api_script = f""" + async () => {{ + const url = "https://www.tiktok.com/api/search/general/full/?keyword={quote(query)}&offset={cursor}&count={limit}&search_source=normal_search&is_filter_search=0"; + try {{ + const res = await fetch(url); + return await res.json(); + }} catch (e) {{ + return {{ error: e.toString() }}; + }} + }} + """ - for i in range(scroll_count): - await page.evaluate("window.scrollBy(0, 1500)") - await asyncio.sleep(1.5) - - # After reaching the offset, scroll a bit more to trigger the specific batch capture - batch_scrolls = (limit // 10) + 2 # Add extra scrolls to be safe - for _ in range(batch_scrolls): - await page.evaluate("window.scrollBy(0, 2000)") # Larger scroll - await asyncio.sleep(1.0) # Faster scroll cadence - - # Wait a bit after scrolling for all responses to settle - await asyncio.sleep(2.5) + data = await page.evaluate(api_script) + # 3. Parse Results + if data and "error" not in data: + items = [] + # Try data list directly (general search) + if data.get("data") and isinstance(data["data"], list): + for item in data["data"]: + if isinstance(item, dict): + if "item" in item: + items.append(item["item"]) + elif "aweme" in item: + items.append(item["aweme"]) + elif "type" in item and item["type"] == 1: # Video type + items.append(item) + # Try itemList (item search) + elif data.get("itemList"): + items = data["itemList"] + elif data.get("item_list"): + items = data["item_list"] except Exception as e: - print(f"DEBUG: Error during search: {e}") + print(f"DEBUG: Search navigation error: {e}") await browser.close() - print(f"DEBUG: Total captured search videos in this batch: {len(captured_videos)}") + print(f"DEBUG: Total captured search videos: {len(captured_videos)}") return captured_videos + + + @staticmethod async def fetch_suggested_accounts(cookies: list, user_agent: str = None, limit: int = 50) -> list: diff --git a/backend/core/tiktok_api_service.py b/backend/core/tiktok_api_service.py new file mode 100644 index 0000000..46d48ae --- /dev/null +++ b/backend/core/tiktok_api_service.py @@ -0,0 +1,450 @@ +""" +TikTok Direct API Service - Fast API calls without browser automation. + +Replaces Playwright crawling with direct HTTP requests to TikTok's internal APIs. +Expected performance: ~100-500ms vs 5-15 seconds with Playwright. +""" + +import httpx +import asyncio +from typing import List, Optional, Dict, Any +from urllib.parse import quote + +from core.playwright_manager import PlaywrightManager + + +class TikTokAPIService: + """ + Direct TikTok API calls for instant data retrieval. + + Key endpoints used: + - /api/user/detail/?uniqueId={username} - Get user profile and secUid + - /api/post/item_list/?secUid={secUid}&count={count} - Get user's videos + - /api/search/general/full/?keyword={query} - Search videos + """ + + BASE_URL = "https://www.tiktok.com" + DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + + @staticmethod + def _build_headers(cookies: List[dict], user_agent: str = None) -> dict: + """Build request headers with cookies and user agent.""" + cookie_str = "; ".join([f"{c['name']}={c['value']}" for c in cookies]) + + return { + "User-Agent": user_agent or TikTokAPIService.DEFAULT_USER_AGENT, + "Referer": "https://www.tiktok.com/", + "Cookie": cookie_str, + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + } + + @staticmethod + def _extract_video_data(item: dict) -> Optional[dict]: + """ + Extract video data from TikTok API response item. + Matches the format used by PlaywrightManager._extract_video_data(). + """ + try: + if not isinstance(item, dict): + return None + + video_id = item.get("id") or item.get("aweme_id") + + # Get author info + author_data = item.get("author", {}) + author = author_data.get("uniqueId") or author_data.get("unique_id") or "unknown" + + # Get description + desc = item.get("desc") or item.get("description") or "" + + # Check if this is a product/shop video + is_shop_video = bool(item.get("products") or item.get("commerce_info") or item.get("poi_info")) + + # Get thumbnail/cover image + thumbnail = None + video_data = item.get("video", {}) + + thumbnail_sources = [ + video_data.get("cover"), + video_data.get("dynamicCover"), + video_data.get("originCover"), + ] + for src in thumbnail_sources: + if src: + thumbnail = src + break + + # Get direct CDN URL + cdn_url = None + cdn_sources = [ + video_data.get("playAddr"), + video_data.get("downloadAddr"), + ] + for src in cdn_sources: + if src: + cdn_url = src + break + + # Video page URL + video_url = f"https://www.tiktok.com/@{author}/video/{video_id}" + + # Get stats + stats = item.get("stats", {}) or item.get("statistics", {}) + views = stats.get("playCount") or stats.get("play_count") or 0 + likes = stats.get("diggCount") or stats.get("digg_count") or 0 + comments = stats.get("commentCount") or stats.get("comment_count") or 0 + shares = stats.get("shareCount") or stats.get("share_count") or 0 + + if video_id and author: + result = { + "id": str(video_id), + "url": video_url, + "author": author, + "description": desc[:200] if desc else f"Video by @{author}" + } + if thumbnail: + result["thumbnail"] = thumbnail + if cdn_url: + result["cdn_url"] = cdn_url + if views: + result["views"] = views + if likes: + result["likes"] = likes + if comments: + result["comments"] = comments + if shares: + result["shares"] = shares + if is_shop_video: + result["has_product"] = True + return result + + except Exception as e: + print(f"DEBUG: Error extracting video data: {e}") + + return None + + @staticmethod + async def get_user_sec_uid(username: str, cookies: List[dict], user_agent: str = None) -> Optional[str]: + """ + Get user's secUid from their profile. + secUid is required for the video list API. + """ + headers = TikTokAPIService._build_headers(cookies, user_agent) + profile_url = f"{TikTokAPIService.BASE_URL}/api/user/detail/?uniqueId={username}" + + try: + async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: + response = await client.get(profile_url, headers=headers) + + if response.status_code != 200: + print(f"DEBUG: Failed to get user profile, status: {response.status_code}") + return None + + data = response.json() + user_info = data.get("userInfo", {}) + user = user_info.get("user", {}) + sec_uid = user.get("secUid") + + if sec_uid: + print(f"DEBUG: Got secUid for @{username}: {sec_uid[:20]}...") + return sec_uid + + except Exception as e: + print(f"DEBUG: Error getting secUid for {username}: {e}") + + return None + + @staticmethod + async def get_user_videos( + username: str, + cookies: List[dict], + user_agent: str = None, + limit: int = 20, + cursor: int = 0 + ) -> List[dict]: + """ + Fetch videos from a user's profile using direct API call. + + Args: + username: TikTok username (without @) + cookies: Auth cookies list + user_agent: Browser user agent + limit: Max videos to return + cursor: Pagination cursor for more videos + + Returns: + List of video dictionaries + """ + print(f"DEBUG: [API] Fetching videos for @{username} (limit={limit})...") + + # Step 1: Get secUid + sec_uid = await TikTokAPIService.get_user_sec_uid(username, cookies, user_agent) + + if not sec_uid: + print(f"DEBUG: [API] Could not get secUid for @{username}") + return [] + + # Step 2: Fetch video list + headers = TikTokAPIService._build_headers(cookies, user_agent) + + # Build video list API URL + video_list_url = ( + f"{TikTokAPIService.BASE_URL}/api/post/item_list/?" + f"secUid={quote(sec_uid)}&" + f"count={min(limit, 35)}&" # TikTok max per request is ~35 + f"cursor={cursor}" + ) + + try: + async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: + response = await client.get(video_list_url, headers=headers) + + if response.status_code != 200: + print(f"DEBUG: [API] Video list failed, status: {response.status_code}") + return [] + + data = response.json() + + # Extract videos from response + items = data.get("itemList", []) or data.get("aweme_list", []) + + videos = [] + for item in items[:limit]: + video_data = TikTokAPIService._extract_video_data(item) + if video_data: + videos.append(video_data) + + print(f"DEBUG: [API] Successfully fetched {len(videos)} videos for @{username}") + return videos + + except Exception as e: + print(f"DEBUG: [API] Error fetching videos for {username}: {e}") + return [] + + @staticmethod + async def search_videos( + query: str, + cookies: List[dict], + user_agent: str = None, + limit: int = 20, + cursor: int = 0 + ) -> List[dict]: + """ + Search for videos using direct API call. + + Args: + query: Search keyword or hashtag + cookies: Auth cookies list + user_agent: Browser user agent + limit: Max videos to return + cursor: Pagination offset + + Returns: + List of video dictionaries + """ + print(f"DEBUG: [API] Searching for '{query}' (limit={limit}, cursor={cursor})...") + + headers = TikTokAPIService._build_headers(cookies, user_agent) + + # Build search API URL + # TikTok uses different search endpoints, try the main one + search_url = ( + f"{TikTokAPIService.BASE_URL}/api/search/general/full/?" + f"keyword={quote(query)}&" + f"offset={cursor}&" + f"search_source=normal_search&" + f"is_filter_search=0&" + f"web_search_code=%7B%22tiktok%22%3A%7B%22client_params_x%22%3A%7B%22search_engine%22%3A%7B%22ies_mt_user_live_video_card_use_498%22%3A1%7D%7D%7D%7D" + ) + + try: + async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: + response = await client.get(search_url, headers=headers) + + if response.status_code != 200: + print(f"DEBUG: [API] Search failed, status: {response.status_code}") + # Try alternative search endpoint + return await TikTokAPIService._search_videos_alt(query, cookies, user_agent, limit, cursor) + + data = response.json() + + # Search results structure + videos = [] + + # Try different response formats + item_list = data.get("data", []) + if not item_list: + item_list = data.get("itemList", []) + if not item_list: + item_list = data.get("item_list", []) + + for item in item_list[:limit]: + # Search results may have nested structure + video_item = item.get("item", item) + video_data = TikTokAPIService._extract_video_data(video_item) + if video_data: + videos.append(video_data) + + if videos: + print(f"DEBUG: [API] Successfully found {len(videos)} videos for '{query}'") + return videos + else: + # Fallback to alternative endpoint + return await TikTokAPIService._search_videos_alt(query, cookies, user_agent, limit, cursor) + + except Exception as e: + print(f"DEBUG: [API] Error searching for {query}: {e}") + return await TikTokAPIService._search_videos_alt(query, cookies, user_agent, limit, cursor) + + @staticmethod + async def _search_videos_alt( + query: str, + cookies: List[dict], + user_agent: str = None, + limit: int = 20, + cursor: int = 0 + ) -> List[dict]: + """ + Alternative search using video-specific endpoint. + """ + print(f"DEBUG: [API] Trying alternative search endpoint...") + + headers = TikTokAPIService._build_headers(cookies, user_agent) + + # Try video-specific search endpoint + search_url = ( + f"{TikTokAPIService.BASE_URL}/api/search/item/full/?" + f"keyword={quote(query)}&" + f"offset={cursor}&" + f"count={min(limit, 30)}" + ) + + try: + async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: + response = await client.get(search_url, headers=headers) + + if response.status_code != 200: + print(f"DEBUG: [API] Alt search also failed, status: {response.status_code}") + return [] + + data = response.json() + + videos = [] + item_list = data.get("itemList", []) or data.get("item_list", []) or data.get("data", []) + + for item in item_list[:limit]: + video_data = TikTokAPIService._extract_video_data(item) + if video_data: + videos.append(video_data) + + print(f"DEBUG: [API] Alt search found {len(videos)} videos") + return videos + + except Exception as e: + print(f"DEBUG: [API] Alt search error: {e}") + return [] + + + @staticmethod + async def get_user_videos_via_ytdlp(username: str, limit: int = 20) -> List[dict]: + """ + Fetch user videos using yt-dlp (Robust fallback). + """ + print(f"DEBUG: [yt-dlp] Fetching videos for @{username}...") + import subprocess + import json + + # Determine yt-dlp path (assume it's in the same python environment) + import sys + import os + + # Helper to find executable + def get_yt_dlp_path(): + # Try same dir as python executable + path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe') + if os.path.exists(path): return path + # Try global + return 'yt-dlp' + + get_yt_dlp_path(), + f"https://www.tiktok.com/@{username}", + # "--flat-playlist", # Disabled to get full metadata (thumbnails) + "--skip-download", # Don't download video files + "--dump-json", + "--playlist-end", str(limit), + "--no-warnings", + "--ignore-errors" # Skip private/removed videos + + try: + # Run async subprocess + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await process.communicate() + + if process.returncode != 0: + print(f"DEBUG: [yt-dlp] Failed: {stderr.decode()}") + return [] + + videos = [] + output = stdout.decode('utf-8') + + for line in output.splitlines(): + try: + if not line.strip(): continue + data = json.loads(line) + + # Map yt-dlp format to our format + video_id = data.get('id') + + # Handle Author Name logic + # yt-dlp sometimes returns numeric ID as uploader_id for profiles. + # We prefer the 'uploader' (nickname) if it looks handle-like, or the original search username. + raw_uploader_id = data.get('uploader_id') + + # Heuristic: If uploader_id is numeric, prefer the search username + if raw_uploader_id and raw_uploader_id.isdigit(): + unique_id = username + else: + unique_id = raw_uploader_id or username + + # yt-dlp might not give full details in flat-playlist mode, + # but usually gives enough for a list + # Construct basic object + video = { + "id": video_id, + "url": data.get('url') or f"https://www.tiktok.com/@{unique_id}/video/{video_id}", + "author": unique_id, + "description": data.get('title') or "", + "thumbnail": data.get('thumbnail'), # Flat playlist might not have this? + "views": data.get('view_count', 0), + "likes": data.get('like_count', 0) + } + + # If thumbnail is missing, we might need to assume or use a placeholder + # or yt-dlp flat playlist sometimes misses it. + # But verifying the 'dump-json' output above, it usually has metadata. + + videos.append(video) + except Exception as parse_err: + continue + + print(f"DEBUG: [yt-dlp] Found {len(videos)} videos") + return videos + + except Exception as e: + print(f"DEBUG: [yt-dlp] Execution error: {e}") + return [] + +# Singleton instance +tiktok_api = TikTokAPIService() diff --git a/backend/debug_api.py b/backend/debug_api.py new file mode 100644 index 0000000..4771e2b --- /dev/null +++ b/backend/debug_api.py @@ -0,0 +1,44 @@ +import asyncio +import httpx +import sys +from core.playwright_manager import PlaywrightManager + +async def test_api(): + print("Loading credentials...") + cookies, user_agent = PlaywrightManager.load_stored_credentials() + + headers = { + "User-Agent": user_agent or "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Referer": "https://www.tiktok.com/", + "Cookie": "; ".join([f"{c['name']}={c['value']}" for c in cookies]), + } + + username = "x.ka.baongoc" + url = f"https://www.tiktok.com/api/user/detail/?uniqueId={username}" + + print(f"Fetching {url}...") + async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: + res = await client.get(url, headers=headers) + print(f"Status: {res.status_code}") + if res.status_code == 200: + try: + data = res.json() + user = data.get("userInfo", {}).get("user", {}) + sec_uid = user.get("secUid") + print(f"SecUid: {sec_uid}") + if not sec_uid: + print("Response body preview:", str(data)[:500]) + except: + print("JSON Decode Failed. Content preview:") + print(res.text[:500]) + else: + print("Response:", res.text[:500]) + +if __name__ == "__main__": + try: + if sys.platform == "win32": + loop = asyncio.ProactorEventLoop() + asyncio.set_event_loop(loop) + loop.run_until_complete(test_api()) + except Exception as e: + print(e) diff --git a/backend/following.json b/backend/following.json index 5ea2dae..5d7379b 100644 --- a/backend/following.json +++ b/backend/following.json @@ -1,5 +1,8 @@ [ "nhythanh_04", "po.trann77", - "tieu_hy26" + "tieu_hy26", + "phamthuy9722r", + "tlin99", + "mjxdj9" ] \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 61d7cf0..b4e1232 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,3 +1,11 @@ +import sys +import asyncio + +# CRITICAL: Set Windows event loop policy BEFORE any other imports +# Playwright requires ProactorEventLoop for subprocess support on Windows +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -5,12 +13,7 @@ from fastapi.responses import FileResponse from contextlib import asynccontextmanager from pathlib import Path from api.routes import auth, feed, download, following, config, user -import sys -import asyncio -# Force Proactor on Windows for Playwright -if sys.platform == "win32": - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) @asynccontextmanager async def lifespan(app: FastAPI): diff --git a/backend/requirements.txt b/backend/requirements.txt index 4904af9..5403571 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,5 +6,6 @@ python-multipart websockets python-dotenv crawl4ai -playwright +playwright==1.49.1 playwright-stealth +httpx \ No newline at end of file diff --git a/backend/run_windows.py b/backend/run_windows.py new file mode 100644 index 0000000..e52d368 --- /dev/null +++ b/backend/run_windows.py @@ -0,0 +1,26 @@ +""" +Windows-compatible startup script for PureStream. +Sets ProactorEventLoop policy BEFORE uvicorn imports anything. +""" +import sys +import asyncio + +# CRITICAL: Must be set before importing uvicorn or any async code +if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + # Also create the loop early + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + print(f"DEBUG: Forced ProactorEventLoop: {type(loop)}") + +# Now import and run uvicorn +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8002, + reload=False, # Disabled: reload subprocess loses ProactorEventLoop on Windows + loop="asyncio" # Use asyncio, which should now use our ProactorEventLoop + ) diff --git a/backend/verify_fix_cookies.py b/backend/verify_fix_cookies.py new file mode 100644 index 0000000..33c5d5f --- /dev/null +++ b/backend/verify_fix_cookies.py @@ -0,0 +1,17 @@ +import asyncio +from backend.core.playwright_manager import PlaywrightManager + +async def test_search(): + print("Testing search_videos with STORED COOKIES...") + cookies, ua = PlaywrightManager.load_stored_credentials() + print(f"Loaded {len(cookies)} cookies. UA: {ua[:50]}...") + + videos = await PlaywrightManager.search_videos("gai xinh nhay", cookies=cookies, user_agent=ua, limit=5) + + print(f"Found {len(videos)} videos.") + for i, v in enumerate(videos): + play_addr = v.get("video", {}).get("play_addr") + print(f"Video {i} Play Addr: {play_addr}") + +if __name__ == "__main__": + asyncio.run(test_search()) diff --git a/backup_project.ps1 b/backup_project.ps1 new file mode 100644 index 0000000..031d329 --- /dev/null +++ b/backup_project.ps1 @@ -0,0 +1,66 @@ +$ErrorActionPreference = "Stop" + +$ProjectDir = Get-Location +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupDir = Join-Path $ProjectDir "backups" +$BackupFile = Join-Path $BackupDir "kv_tiktok_backup_$Timestamp.zip" + +# Create backup directory if it doesn't exist +if (-not (Test-Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null + Write-Host "Created backup directory: $BackupDir" -ForegroundColor Cyan +} + +Write-Host "Starting backup of $ProjectDir..." -ForegroundColor Cyan +Write-Host "Target file: $BackupFile" -ForegroundColor Cyan + +# Exclude list (Patterns to ignore) +$ExcludePatterns = @( + "^\.git", + "^\.venv", + "^node_modules", + "__pycache__", + "^backups", + "\.log$", + "backend\\downloads", + "backend\\cache", + "backend\\session", + "frontend\\dist" +) + +# Get files to zip +$FilesToZip = Get-ChildItem -Path $ProjectDir -Recurse | Where-Object { + $relativePath = $_.FullName.Substring($ProjectDir.Path.Length + 1) + $shouldExclude = $false + + foreach ($pattern in $ExcludePatterns) { + if ($relativePath -match $pattern) { + $shouldExclude = $true + break + } + } + + # Also exclude the backup directory itself and any zip files inside root (if active) + if ($relativePath -like "backups\*") { $shouldExclude = $true } + + return -not $shouldExclude +} + +if ($FilesToZip.Count -eq 0) { + Write-Error "No files found to backup!" +} + +# Compress +Write-Host "Compressing $($FilesToZip.Count) files..." -ForegroundColor Yellow +$FilesToZip | Compress-Archive -DestinationPath $BackupFile -Force + +if (Test-Path $BackupFile) { + $Item = Get-Item $BackupFile + $SizeMB = [math]::Round($Item.Length / 1MB, 2) + Write-Host "Backup created successfully!" -ForegroundColor Green + Write-Host "Location: $BackupFile" + Write-Host "Size: $SizeMB MB" +} +else { + Write-Error "Backup failed!" +} diff --git a/backup_project.sh b/backup_project.sh new file mode 100644 index 0000000..cf226ce --- /dev/null +++ b/backup_project.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Configuration +PROJECT_DIR="$(pwd)" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") +BACKUP_DIR="${PROJECT_DIR}/backups" +BACKUP_FILE="${BACKUP_DIR}/kv_tiktok_backup_${TIMESTAMP}.zip" + +# Create backup directory if it doesn't exist +if [ ! -d "$BACKUP_DIR" ]; then + echo "Creating backup directory: $BACKUP_DIR" + mkdir -p "$BACKUP_DIR" +fi + +echo "Starting backup of ${PROJECT_DIR}..." +echo "Target file: ${BACKUP_FILE}" + +# Zip the project contents, excluding heavy/generated folders +# Using -r for recursive, -q for quiet (optional), -x to exclude patterns +zip -r "$BACKUP_FILE" . \ + -x "*.git*" \ + -x "*.venv*" \ + -x "*node_modules*" \ + -x "*__pycache__*" \ + -x "*.DS_Store" \ + -x "*backend/downloads*" \ + -x "*backend/cache*" \ + -x "*backend/session*" \ + -x "*frontend/dist*" \ + -x "*backups*" \ + -x "*.log" + +if [ $? -eq 0 ]; then + echo "โœ… Backup created successfully!" + echo "๐Ÿ“‚ Location: ${BACKUP_FILE}" + + # Show file size + if [[ "$OSTYPE" == "darwin"* ]]; then + SIZE=$(stat -f%z "$BACKUP_FILE") + else + SIZE=$(stat -c%s "$BACKUP_FILE") + fi + # Convert to MB + SIZE_MB=$(echo "scale=2; $SIZE / 1024 / 1024" | bc) + echo "๐Ÿ“ฆ Size: ${SIZE_MB} MB" +else + echo "โŒ Backup failed!" + exit 1 +fi diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..7974d69 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +# PureStream Development Docker Compose +# Uses Dockerfile.dev which COPIES files to avoid Synology Drive filesystem issues + +services: + backend: + container_name: purestream_dev + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "8002:8002" + volumes: + # Only mount data directories (not code) + - purestream_cache:/app/cache + - purestream_session:/app/session + shm_size: '2gb' + +volumes: + purestream_cache: + purestream_session: + + # NOTE: + # - Frontend is served by backend at http://localhost:8002 + # - Code changes require rebuild: docker-compose -f docker-compose.dev.yml up --build diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6a9e031..9eeed34 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,11 +11,14 @@ "artplayer": "^5.3.0", "axios": "^1.13.2", "clsx": "^2.1.1", + "esbuild": "^0.27.2", "framer-motion": "^12.23.26", - "lucide-react": "^0.561.0", + "hls.js": "^1.6.15", + "lucide-react": "^0.563.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.2", + "rollup": "^4.56.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" }, @@ -34,7 +37,10 @@ "tailwindcss": "^3.4.10", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^5.4.21" + }, + "optionalDependencies": { + "@rollup/rollup-win32-x64-msvc": "^4.56.0" } }, "node_modules/@alloc/quick-lru": { @@ -52,8 +58,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { @@ -67,8 +71,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -77,10 +79,9 @@ }, "node_modules/@babel/core": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -108,8 +109,6 @@ }, "node_modules/@babel/generator": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -125,8 +124,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { @@ -142,8 +139,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -152,8 +147,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { @@ -166,8 +159,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { @@ -184,8 +175,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -194,8 +183,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -204,8 +191,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -214,8 +199,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -224,8 +207,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { @@ -238,8 +219,6 @@ }, "node_modules/@babel/parser": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -254,8 +233,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -270,8 +247,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -286,8 +261,6 @@ }, "node_modules/@babel/template": { "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { @@ -301,8 +274,6 @@ }, "node_modules/@babel/traverse": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { @@ -320,8 +291,6 @@ }, "node_modules/@babel/types": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { @@ -333,400 +302,423 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -744,8 +736,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -757,8 +747,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -882,8 +870,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -892,8 +878,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -906,8 +890,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -920,8 +902,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -934,8 +914,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -945,8 +923,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -956,8 +932,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -966,15 +940,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1022,8 +992,6 @@ }, "node_modules/@remix-run/router": { "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1037,13 +1005,12 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1051,13 +1018,12 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1065,13 +1031,12 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1079,13 +1044,12 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1093,13 +1057,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1107,13 +1070,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1121,13 +1083,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1135,13 +1096,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1149,13 +1109,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1163,13 +1122,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1177,13 +1135,25 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1191,13 +1161,25 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1205,13 +1187,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1219,13 +1200,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1233,13 +1213,12 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1247,13 +1226,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1261,27 +1239,38 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1289,13 +1278,12 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1303,13 +1291,12 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1317,13 +1304,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1331,13 +1317,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1393,7 +1378,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1404,11 +1388,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1426,6 +1411,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1442,20 +1428,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", - "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/type-utils": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1465,15 +1451,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.51.0", + "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -1481,17 +1465,18 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", - "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1506,15 +1491,15 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", - "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.51.0", - "@typescript-eslint/types": "^8.51.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1528,14 +1513,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", - "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0" + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1546,9 +1531,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", - "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", "dev": true, "license": "MIT", "engines": { @@ -1563,17 +1548,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", - "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.2.0" + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1588,9 +1573,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", - "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", "dev": true, "license": "MIT", "engines": { @@ -1602,21 +1587,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", - "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.51.0", - "@typescript-eslint/tsconfig-utils": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/visitor-keys": "8.51.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.2.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1669,16 +1654,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", - "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.51.0", - "@typescript-eslint/types": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1693,13 +1678,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", - "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1712,8 +1697,6 @@ }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { @@ -1737,6 +1720,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1746,8 +1730,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1756,8 +1738,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1773,8 +1753,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1789,15 +1767,11 @@ }, "node_modules/any-promise": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -1810,8 +1784,6 @@ }, "node_modules/arg": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true, "license": "MIT" }, @@ -1833,8 +1805,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/autoprefixer": { @@ -1876,8 +1846,6 @@ }, "node_modules/axios": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1894,8 +1862,6 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1904,8 +1870,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -1917,8 +1881,6 @@ }, "node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1928,8 +1890,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -1959,6 +1919,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1998,8 +1959,6 @@ }, "node_modules/camelcase-css": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, "license": "MIT", "engines": { @@ -2008,8 +1967,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", "dev": true, "funding": [ { @@ -2029,8 +1986,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -2046,8 +2001,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -2071,8 +2024,6 @@ }, "node_modules/chokidar/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -2084,8 +2035,6 @@ }, "node_modules/clsx": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -2093,8 +2042,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2106,15 +2053,11 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -2135,22 +2078,16 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -2164,8 +2101,6 @@ }, "node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { @@ -2184,8 +2119,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2202,15 +2135,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -2218,22 +2147,16 @@ }, "node_modules/didyoumean": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true, "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2246,15 +2169,11 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2271,8 +2190,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2283,8 +2200,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2297,42 +2212,44 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -2360,10 +2277,9 @@ }, "node_modules/eslint": { "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2420,8 +2336,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -2433,8 +2347,6 @@ }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2460,8 +2372,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2473,8 +2383,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2491,8 +2399,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2517,8 +2423,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2527,8 +2431,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2537,15 +2439,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2561,8 +2459,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -2581,15 +2477,11 @@ }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -2611,8 +2503,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2624,8 +2514,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -2641,8 +2529,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -2662,8 +2548,6 @@ }, "node_modules/follow-redirects": { "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -2682,8 +2566,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2698,8 +2580,6 @@ }, "node_modules/fraction.js": { "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { @@ -2712,8 +2592,6 @@ }, "node_modules/framer-motion": { "version": "12.23.26", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", - "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", "license": "MIT", "dependencies": { "motion-dom": "^12.23.23", @@ -2739,10 +2617,6 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -2773,8 +2647,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2797,8 +2669,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -2810,8 +2680,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -2836,8 +2704,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2848,8 +2714,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -2858,8 +2722,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2885,8 +2747,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2895,6 +2755,10 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2907,8 +2771,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2934,8 +2796,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -2947,8 +2807,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -2963,8 +2821,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -2986,8 +2842,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -2996,8 +2850,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, @@ -3007,20 +2859,17 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3045,29 +2894,21 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -3079,8 +2920,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -3089,8 +2928,6 @@ }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3098,8 +2935,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3112,8 +2947,6 @@ }, "node_modules/lilconfig": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "license": "MIT", "engines": { @@ -3125,15 +2958,11 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -3148,8 +2977,6 @@ }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, @@ -3176,9 +3003,9 @@ } }, "node_modules/lucide-react": { - "version": "0.561.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", - "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3195,8 +3022,6 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -3205,8 +3030,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -3219,8 +3042,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -3228,8 +3049,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -3240,8 +3059,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -3253,17 +3070,15 @@ }, "node_modules/motion-dom": { "version": "12.23.23", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", - "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", "license": "MIT", "dependencies": { "motion-utils": "^12.23.6" } }, "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "version": "12.27.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.27.2.tgz", + "integrity": "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==", "license": "MIT" }, "node_modules/ms": { @@ -3306,8 +3121,6 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -3320,8 +3133,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -3330,8 +3141,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -3359,8 +3168,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -3377,8 +3184,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3393,8 +3198,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -3409,8 +3212,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -3432,8 +3233,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -3442,15 +3241,11 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -3469,8 +3264,6 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, "license": "MIT", "engines": { @@ -3489,8 +3282,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -3507,6 +3298,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3518,8 +3310,6 @@ }, "node_modules/postcss-import": { "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "license": "MIT", "dependencies": { @@ -3605,8 +3395,6 @@ }, "node_modules/postcss-nested": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, "funding": [ { @@ -3631,8 +3419,6 @@ }, "node_modules/postcss-selector-parser": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { @@ -3645,8 +3431,6 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT" }, @@ -3699,9 +3483,8 @@ }, "node_modules/react": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3711,9 +3494,8 @@ }, "node_modules/react-dom": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3724,8 +3506,6 @@ }, "node_modules/react-refresh": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { @@ -3734,8 +3514,6 @@ }, "node_modules/react-router": { "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.1" @@ -3749,8 +3527,6 @@ }, "node_modules/react-router-dom": { "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "license": "MIT", "dependencies": { "@remix-run/router": "1.23.1", @@ -3766,8 +3542,6 @@ }, "node_modules/read-cache": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, "license": "MIT", "dependencies": { @@ -3776,8 +3550,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { @@ -3810,8 +3582,6 @@ }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -3830,10 +3600,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3846,35 +3615,36 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -3897,8 +3667,6 @@ }, "node_modules/scheduler": { "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -3906,8 +3674,6 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -3916,8 +3682,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -3939,8 +3703,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3985,8 +3747,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3998,8 +3758,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { @@ -4021,8 +3779,6 @@ }, "node_modules/tailwindcss": { "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4069,8 +3825,6 @@ }, "node_modules/thenify-all": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, "license": "MIT", "dependencies": { @@ -4121,6 +3875,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4130,8 +3885,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4142,9 +3895,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", - "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -4169,8 +3922,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -4186,6 +3937,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4195,16 +3947,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", - "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.51.0", - "@typescript-eslint/parser": "8.51.0", - "@typescript-eslint/typescript-estree": "8.51.0", - "@typescript-eslint/utils": "8.51.0" + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4220,15 +3972,11 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -4258,8 +4006,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4268,8 +4014,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, @@ -4279,6 +4023,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -4333,10 +4078,438 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -4351,8 +4524,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -4361,15 +4532,11 @@ }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -4381,8 +4548,6 @@ }, "node_modules/zustand": { "version": "5.0.9", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", - "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/frontend/package.json b/frontend/package.json index 6acafbb..c4edb1d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,11 +13,14 @@ "artplayer": "^5.3.0", "axios": "^1.13.2", "clsx": "^2.1.1", + "esbuild": "^0.27.2", "framer-motion": "^12.23.26", - "lucide-react": "^0.561.0", + "hls.js": "^1.6.15", + "lucide-react": "^0.563.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.2", + "rollup": "^4.56.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" }, @@ -36,6 +39,9 @@ "tailwindcss": "^3.4.10", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^5.4.21" + }, + "optionalDependencies": { + "@rollup/rollup-win32-x64-msvc": "^4.56.0" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1738178..71a514f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,7 +23,9 @@ import { Feed } from './components/Feed'; const Dashboard = () => { return (
- + + } /> +
) } @@ -44,7 +46,7 @@ function App() { } /> } /> diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index d8502fe..0b7f3f5 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -1,14 +1,18 @@ import React, { useState, useEffect, useRef } from 'react'; +import { motion } from 'framer-motion'; import { VideoPlayer } from './VideoPlayer'; +import { SkeletonFeed } from './SkeletonFeed'; +import { Sidebar } from './Sidebar'; import type { Video, UserProfile } from '../types'; +import { SearchSkeleton } from './SearchSkeleton'; import axios from 'axios'; import { API_BASE_URL } from '../config'; -import { Search, X, Plus } from 'lucide-react'; +import { Search, X, Plus, ArrowLeft } from 'lucide-react'; import { videoPrefetcher } from '../utils/videoPrefetch'; import { feedLoader } from '../utils/feedLoader'; type ViewState = 'login' | 'loading' | 'feed'; -type TabType = 'foryou' | 'following' | 'search'; +type TabType = 'foryou' | 'search' | 'following' | 'profile'; // Suggested categories for Following tab const SUGGESTED_CATEGORIES = [ @@ -102,6 +106,41 @@ export const Feed: React.FC = () => { // Following state const [following, setFollowing] = useState([]); const [newFollowInput, setNewFollowInput] = useState(''); + const [followingVideos, setFollowingVideos] = useState([]); + const [isFollowingLoading, setIsFollowingLoading] = useState(false); + + // Fetch following feed + const loadFollowingFeed = async () => { + if (following.length === 0) return; + setIsFollowingLoading(true); + try { + const res = await axios.get(`${API_BASE_URL}/following/feed?limit_per_user=3`); + setFollowingVideos(res.data); + if (res.data.length === 0) { + // If no videos returned (e.g. users have no new videos), maybe show a toast? + } + } catch (err) { + console.error('Error loading following feed:', err); + } finally { + setIsFollowingLoading(false); + } + }; + + // Reload following feed when tab changes to 'following' + useEffect(() => { + if (activeTab === 'following' && following.length > 0 && followingVideos.length === 0) { + loadFollowingFeed(); + } + }, [activeTab, following]); + + // Update feed when a new user is followed (if explicitly requested) + const refreshType = useRef<'follow' | null>(null); + useEffect(() => { + if (refreshType.current === 'follow' && activeTab === 'following') { + loadFollowingFeed(); + refreshType.current = null; + } + }, [following]); // Suggested profiles with real data const [suggestedProfiles, setSuggestedProfiles] = useState([]); @@ -109,7 +148,6 @@ export const Feed: React.FC = () => { const [suggestedLimit, setSuggestedLimit] = useState(12); const [showHeader, setShowHeader] = useState(false); const [isFollowingFeed, setIsFollowingFeed] = useState(false); - const [isVideoPaused, setIsVideoPaused] = useState(true); // Tracks if current video is paused (controls UI visibility) // Lazy load - start with 12 // Search state @@ -118,22 +156,16 @@ export const Feed: React.FC = () => { const [isSearching, setIsSearching] = useState(false); const [error, setError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); - const [searchMatchedUser, setSearchMatchedUser] = useState(null); + // Global mute state - persists across video scrolling const [isMuted, setIsMuted] = useState(true); - // Profile View state - grid of videos from a specific user - const [profileViewUsername, setProfileViewUsername] = useState(null); - const [profileVideos, setProfileVideos] = useState([]); - const [profileLoading, setProfileLoading] = useState(false); - const [profileHasMore, setProfileHasMore] = useState(true); - const [profileUserData, setProfileUserData] = useState(null); - const profileGridRef = useRef(null); + // Loading timer state - shows elapsed time during video crawling const [loadingElapsed, setLoadingElapsed] = useState(0); - const loadingTimerRef = useRef(null); + const loadingTimerRef = useRef | null>(null); // Start/stop loading timer helper functions const startLoadingTimer = () => { @@ -200,11 +232,9 @@ export const Feed: React.FC = () => { const isRightSwipe = distanceX < -minSwipeDistance; if (isLeftSwipe) { - if (activeTab === 'foryou') { setActiveTab('following'); setShowHeader(true); } - else if (activeTab === 'following') { setActiveTab('search'); setShowHeader(true); } + if (activeTab === 'foryou') { setActiveTab('search'); setShowHeader(true); } } else if (isRightSwipe) { - if (activeTab === 'search') { setActiveTab('following'); setShowHeader(true); } - else if (activeTab === 'following') { setActiveTab('foryou'); setShowHeader(true); } + if (activeTab === 'search') { setActiveTab('foryou'); setShowHeader(true); } } else { // Minor movement - Do nothing (Tap is handled by video click) } @@ -226,12 +256,12 @@ export const Feed: React.FC = () => { } }, [viewState]); - // Load suggested profiles when switching to Following tab + // Load suggested profiles on first load useEffect(() => { - if (activeTab === 'following' && suggestedProfiles.length === 0 && !loadingProfiles) { + if (suggestedProfiles.length === 0 && !loadingProfiles) { loadSuggestedProfiles(); } - }, [activeTab]); + }, []); // Keyboard arrow navigation for desktop useEffect(() => { @@ -243,12 +273,10 @@ export const Feed: React.FC = () => { if (e.key === 'ArrowRight') { e.preventDefault(); - if (activeTab === 'foryou') setActiveTab('following'); - else if (activeTab === 'following') setActiveTab('search'); + if (activeTab === 'foryou') setActiveTab('search'); } else if (e.key === 'ArrowLeft') { e.preventDefault(); - if (activeTab === 'search') setActiveTab('following'); - else if (activeTab === 'following') setActiveTab('foryou'); + if (activeTab === 'search') setActiveTab('foryou'); } }; @@ -286,7 +314,7 @@ export const Feed: React.FC = () => { setLoadingProfiles(true); try { // Try the dynamic suggested API first (auto-updates from TikTok Vietnam) - const res = await axios.get(`${API_BASE_URL}/user/suggested?limit=50`); + const res = await axios.get(`${API_BASE_URL}/user/suggested?limit=20`); const accounts = res.data.accounts || []; if (accounts.length > 0) { @@ -332,24 +360,6 @@ export const Feed: React.FC = () => { try { const { data } = await axios.get(`${API_BASE_URL}/following`); setFollowing(data); - - // If on following tab, load actual feed instead of just profiles - if (activeTab === 'following') { - setIsFetching(true); - try { - const res = await axios.get(`${API_BASE_URL}/following/feed`); - if (res.data && res.data.length > 0) { - setVideos(res.data); - setCurrentIndex(0); - setIsFollowingFeed(true); - setActiveTab('foryou'); // Switch to feed view but with following content - } - } catch (e) { - console.error('Error loading following feed:', e); - } finally { - setIsFetching(false); - } - } } catch (error) { console.error('Error loading following list:', error); } @@ -414,37 +424,55 @@ export const Feed: React.FC = () => { const loadFeed = async () => { setViewState('loading'); - setError(null); + // Prevent double loading + if (videos.length > 0) return; try { - const videos = await feedLoader.loadFeedWithOptimization( - false, + // 1. FAST LOAD (First 4 videos) - Instant render + const initialVideos = await feedLoader.loadFeedWithOptimization( + true, // fast=true (loaded: Video[]) => { if (loaded.length > 0) { setVideos(loaded); setViewState('feed'); - - // Start prefetching first 3 videos immediately + // Prefetch immediately videoPrefetcher.prefetchInitialBatch(loaded, 3); } } ); - if (videos.length === 0) { - // If authenticated but no videos, stay in feed view but show empty state - // Do NOT go back to login, as that confuses the user (they are logged in) - console.warn('Feed empty, but authenticated.'); - setViewState('feed'); - setError('No videos found. Pull to refresh or try searching.'); + if (initialVideos.length === 0) { + console.warn('Fast feed empty, trying full load...'); } + + // 2. BACKGROUND LOAD (Next 15-20 videos) - Seamless fill + // We wait a tiny bit to let the UI settle, then fetch more + setTimeout(async () => { + const moreVideos = await feedLoader.loadFeedWithOptimization(false, undefined, true); // skipCache=true + setVideos(prev => { + const combined = [...prev, ...moreVideos]; + const uniqueMap = new Map(); + combined.forEach(v => { + if (!uniqueMap.has(v.id)) uniqueMap.set(v.id, v); + }); + return Array.from(uniqueMap.values()); + }); + }, 1000); + + if (initialVideos.length === 0) { + // Error handling if both fail handled by UI state + if (videos.length === 0) { + setError('No videos found.'); + } + } + } catch (err: any) { console.error('Feed load failed:', err); - // Only go back to login if it's explicitly an Auth error (401) + // ... existing error handling ... if (err.response?.status === 401) { setError('Session expired. Please login again.'); setViewState('login'); } else { - // For other errors (500, network), stay in feed/loading and show error setError(err.response?.data?.detail || 'Failed to load feed'); setViewState('feed'); } @@ -479,10 +507,17 @@ export const Feed: React.FC = () => { const newVideos = await feedLoader.loadFeedWithOptimization(false, undefined, true); setVideos(prev => { - const existingIds = new Set(prev.map(v => v.id)); - const unique = newVideos.filter((v: Video) => !existingIds.has(v.id)); - if (unique.length === 0) setHasMore(false); - return [...prev, ...unique]; + const combined = [...prev, ...newVideos]; + const uniqueMap = new Map(); + combined.forEach(v => { + if (!uniqueMap.has(v.id)) uniqueMap.set(v.id, v); + }); + + const unique = Array.from(uniqueMap.values()); + // Calculate if we actually added anything new to determine hasMore + if (unique.length === prev.length) setHasMore(false); + + return unique; }); } catch (err) { console.error('Failed to load more:', err); @@ -504,105 +539,8 @@ export const Feed: React.FC = () => { handleSearch(false, `@${username}`); }; - // Open profile view with video grid - const openProfileView = async (username: string) => { - const cleanUsername = username.replace('@', ''); - setProfileViewUsername(cleanUsername); - setProfileVideos([]); - setProfileLoading(true); - setProfileHasMore(true); - setProfileUserData(null); - startLoadingTimer(); // Start countdown timer - // Pause the currently playing video by switching active tab temporarily - // This triggers VideoPlayer's isActive=false which pauses the video - setActiveTab('following'); // Switch away from 'foryou' to pause video - try { - // Fetch user profile data first (show header ASAP) - const profileRes = await axios.get(`${API_BASE_URL}/user/profile?username=${cleanUsername}`); - setProfileUserData(profileRes.data); - - // Fetch videos progressively - load smaller batches and show immediately - const batchSize = 5; - let totalFetched = 0; - const maxVideos = 20; - - while (totalFetched < maxVideos) { - const videosRes = await axios.get(`${API_BASE_URL}/user/videos?username=${cleanUsername}&limit=${batchSize}&offset=${totalFetched}`); - const newVideos = videosRes.data.videos || []; - - if (newVideos.length === 0) { - setProfileHasMore(false); - break; - } - - // Append videos immediately as they load (progressive loading) - filter duplicates - setProfileVideos(prev => { - const existingIds = new Set(prev.map(v => v.id)); - const uniqueNewVideos = newVideos.filter((v: Video) => !existingIds.has(v.id)); - return [...prev, ...uniqueNewVideos]; - }); - totalFetched += newVideos.length; - - // If we got less than batch size, no more videos - if (newVideos.length < batchSize) { - setProfileHasMore(false); - break; - } - } - - // Check if there might be more videos beyond initial 20 - if (totalFetched >= maxVideos) { - setProfileHasMore(true); - } - } catch (err) { - console.error('Error loading profile:', err); - setError('Failed to load profile'); - } finally { - setProfileLoading(false); - stopLoadingTimer(); // Stop countdown timer - } - }; - - // Load more profile videos (lazy load) - const loadMoreProfileVideos = async () => { - if (!profileViewUsername || profileLoading || !profileHasMore) return; - setProfileLoading(true); - - try { - // Use offset/cursor for pagination - const offset = profileVideos.length; - const videosRes = await axios.get(`${API_BASE_URL}/user/videos?username=${profileViewUsername}&limit=20&offset=${offset}`); - const newVideos = videosRes.data.videos || []; - - if (newVideos.length === 0) { - setProfileHasMore(false); - } else { - setProfileVideos(prev => [...prev, ...newVideos]); - setProfileHasMore(newVideos.length >= 20); - } - } catch (err) { - console.error('Error loading more profile videos:', err); - } finally { - setProfileLoading(false); - } - }; - - // Close profile view - const closeProfileView = () => { - setProfileViewUsername(null); - setProfileVideos([]); - setProfileUserData(null); - }; - - // Handle profile grid scroll for lazy loading - const handleProfileGridScroll = (e: React.UIEvent) => { - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - if (scrollHeight - scrollTop <= clientHeight + 200 && profileHasMore && !profileLoading) { - loadMoreProfileVideos(); - } - }; // Direct keyword search const searchByKeyword = async (keyword: string) => { @@ -611,84 +549,149 @@ export const Feed: React.FC = () => { handleSearch(false, keyword); }; - const handleSearch = async (isMore = false, overrideInput?: string) => { - const inputToSearch = overrideInput || searchInput; - if (!inputToSearch.trim() || isSearching) return; + // ... (existing imports) + // ... + + // Use ref for synchronous locking to prevent race conditions during rapid scrolling + const isSearchingRef = useRef(false); + + const handleSearch = async (isMore = false, overrideInput?: string, explicitCursor?: number) => { + const inputToSearch = overrideInput || searchInput; + + // Critical: Check ref instead of state to prevent rapid-fire duplicate requests + if (!inputToSearch.trim() || (isSearchingRef.current && !explicitCursor)) return; + + const cacheKey = `search_${inputToSearch.toLowerCase().trim()}`; + + // Check cache first (only for new searches, not "load more") + // DISABLE CACHE: Forcing fresh search to respect new backend logic (smart user lookup) + /* + if (!isMore && !explicitCursor) { + const cached = localStorage.getItem(cacheKey); + if (cached) { + try { + const { results, timestamp } = JSON.parse(cached); + // Use cache if less than 5 minutes old + if (Date.now() - timestamp < 5 * 60 * 1000 && results.length > 0) { + // Deduplicate cache just in case + const uniqueResults = Array.from(new Map(results.map((v: Video) => [v.id, v])).values()) as Video[]; + setSearchResults(uniqueResults); + + setSearchHasMore(uniqueResults.length >= 20); + console.log('Using cached results for:', inputToSearch); + return; + } + } catch (e) { + console.log('Cache parse error, fetching fresh'); + } + } + } + */ + + // Lock immediately + isSearchingRef.current = true; setIsSearching(true); - setError(null); - startLoadingTimer(); // Start countdown timer + + if (!isMore) setError(null); + if (!isMore) startLoadingTimer(); // Clear previous results immediately if starting a new search - if (!isMore) { + if (!isMore && !explicitCursor) { setSearchResults([]); - setSearchMatchedUser(null); + setSearchCursor(0); } try { - const startCursor = isMore ? searchCursor : 0; + const startCursor = explicitCursor ?? (isMore ? searchCursor : 0); const cleanQuery = inputToSearch.startsWith('@') ? inputToSearch.substring(1) : inputToSearch; - // Step 1: Try to find a matching user profile (only on first search) - if (!isMore) { - try { - const profileRes = await axios.get(`${API_BASE_URL}/user/profile?username=${encodeURIComponent(cleanQuery)}`); - if (profileRes.data && profileRes.data.username) { - setSearchMatchedUser(profileRes.data); - console.log('Found matching user:', profileRes.data.username); - } - } catch (profileErr) { - console.log('No matching user for:', cleanQuery); - setSearchMatchedUser(null); - } - } + // Optimization: Adaptive Batching + // First load: 4 videos (very fast). Subsequent loads: 20 videos (More efficient for heavier backend). + const batchSize = (!isMore && startCursor === 0) ? 4 : 20; - // Step 2: Always fetch related/suggested videos progressively - const batchSize = 10; - let totalFetched = 0; - const maxVideos = 50; + console.log(`Fetching batch: limit=${batchSize}, cursor=${startCursor}`); - while (totalFetched < maxVideos) { - const endpoint = `${API_BASE_URL}/user/search?query=${encodeURIComponent(cleanQuery)}&limit=${batchSize}&cursor=${startCursor + totalFetched}`; - const { data } = await axios.get(endpoint); - const newVideos = data.videos || []; + const endpoint = `${API_BASE_URL}/user/search?query=${encodeURIComponent(cleanQuery)}&limit=${batchSize}&cursor=${startCursor}`; - if (newVideos.length === 0) { - setSearchHasMore(false); - break; - } + const { data } = await axios.get(endpoint); + const newVideos = data.videos || []; - // Filter out duplicates before adding + if (newVideos.length === 0) { + if (!isMore) setSearchHasMore(false); + } else { + // Filter out duplicates (Robust) setSearchResults(prev => { - const existingIds = new Set(prev.map(v => v.id)); - const uniqueNewVideos = newVideos.filter((v: Video) => !existingIds.has(v.id)); - return isMore || totalFetched > 0 ? [...prev, ...uniqueNewVideos] : uniqueNewVideos; - }); - totalFetched += newVideos.length; - setSearchCursor(data.cursor || startCursor + totalFetched); + // Create a map to ensure uniqueness by ID, preferring newer items (or keep existing?) + // Usually we want to keep existing to verify stability? + // Actually, if we are appending, we want new items. + // Let's us a Map to dedupe EVERYTHING. + const combined = [...prev, ...newVideos]; + const uniqueMap = new Map(); + combined.forEach(v => { + if (!uniqueMap.has(v.id)) { + uniqueMap.set(v.id, v); + } + }); + + const updated = Array.from(uniqueMap.values()); + + // Cache the results (only if we have some) + if (!isMore && updated.length > 0) { + localStorage.setItem(cacheKey, JSON.stringify({ + results: updated, + timestamp: Date.now() + })); + } + + return updated; + }); + + const nextCursor = data.cursor || startCursor + newVideos.length; + setSearchCursor(nextCursor); + + // Adaptive Logic: If this was the first fast batch, immediately trigger the next big batch + if (!isMore && startCursor === 0 && newVideos.length > 0) { + console.log("Fast batch loaded, triggering next heavy batch..."); + setTimeout(() => { + // Pass explicit cursor and force a new request regardless of lock state + // But we must respect the lock mechanism basically? + // Actually, if we are calling recursively, we are still "searching" technically? + // No, the previous request finished. We unset lock in finally block. + // So the timeout will run AFTER finally. + handleSearch(true, undefined, nextCursor); + }, 50); + } + + // Check if we reached the end if (newVideos.length < batchSize) { setSearchHasMore(false); - break; + } else { + setSearchHasMore(true); } } - if (totalFetched >= maxVideos) { - setSearchHasMore(true); - } - } catch (err) { console.error('Search failed:', err); - setError('Search failed. Please try again.'); + if (!isMore) setError('Search failed. Please try again.'); } finally { + // Release lock + isSearchingRef.current = false; setIsSearching(false); - stopLoadingTimer(); // Stop countdown timer + stopLoadingTimer(); } }; const handleSearchScroll = (e: React.UIEvent) => { const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - if (scrollHeight - scrollTop <= clientHeight + 100 && searchHasMore && !isSearching) { + + // Infinite Scrolling: Trigger at 2/3 (66%) of the list + // Formula: if scrolledAmount + viewHeight > totalHeight * 0.66 + const threshold = scrollHeight * 0.66; + + if ((scrollTop + clientHeight) > threshold && searchHasMore && !isSearching) { + console.log('Infinite Scroll Triggered'); handleSearch(true); } }; @@ -700,12 +703,7 @@ export const Feed: React.FC = () => { {/* Header */}
-
-
- - - -
+ {/* Logo removed as requested */}

PureStream

Ad-free TikTok viewing

@@ -817,697 +815,546 @@ export const Feed: React.FC = () => { // ========== LOADING VIEW ========== if (viewState === 'loading') { - return ( -
-
-
-
-
-
-
- - - -
-
-
-

Connecting to TikTok...

-
- ); + return ; } // ========== FEED VIEW WITH TABS ========== return ( -
- {/* Tab Navigation */} - {/* Tab Navigation - Hidden by default, show on toggle/swipe */} -
-
- - - -
-
+
+ {/* Desktop Sidebar */} + { + if (tab === 'profile') { + // Handle profile click if needed, or just open current user profile + } else { + setActiveTab(tab); + if (tab === 'foryou' && videos.length === 0) loadFeed(); + } + }} + onLogout={handleLogout} + /> - {/* Logout Button - Left Corner Icon */} - {/* Logout Button / Back Button Logic */} - {/* "make the go back button on the top right conner, replace, swith from the log out button" */} - - {!isInSearchPlayback ? ( - - ) : null} - - {/* FOR YOU TAB */} -
- {/* Video Counter - Shows loading state with blink effect */} -
- - {isFetching ? ( - - Loading {currentIndex + 1}/{videos.length}... - - ) : ( - <> - {currentIndex + 1} / {videos.length} - {hasMore && +} - - )} - -
- - {/* Video Feed */} -
- {videos.map((video, index) => ( -
- {Math.abs(index - currentIndex) <= 1 ? ( - openProfileView(author)} - isMuted={isMuted} - onMuteToggle={() => setIsMuted(prev => !prev)} - onPauseChange={(paused) => { - setIsVideoPaused(paused); - setShowHeader(paused); // Show top bar when video is paused - }} - /> - ) : ( - /* Lightweight Placeholder */ -
- {video.thumbnail ? ( - <> - -
-
-
- - ) : ( -
- )} -
- )} -
- ))} -
-
- - {/* FOLLOWING TAB - Minimal Style */} -
-
- - {/* Minimal Add Input */} -
- setNewFollowInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleAddFollow()} - placeholder="Add @username to follow..." - className="w-full bg-transparent border-b-2 border-white/20 focus:border-white/60 px-0 py-4 text-white text-lg focus:outline-none transition-colors placeholder:text-white/30" - /> + {/* Main Content Area */} +
+ {/* Tab Navigation (Mobile Only) */} + {/* Tab Navigation - Hidden by default, show on toggle/swipe */} +
+
+
+
- {/* My Following - Minimal chips */} - {following.length > 0 && ( -
-

Following

-
- {following.map(user => ( -
- - + {/* Logout Button - Left Corner Icon */} + {/* Logout Button / Back Button Logic */} + {/* "make the go back button on the top right conner, replace, swith from the log out button" */} + + {!isInSearchPlayback ? ( + + ) : null} + + {/* FOR YOU TAB */} +
+ {/* Video Counter - Shows loading state with blink effect */} +
+ + {isFetching ? ( + + Loading {currentIndex + 1}/{videos.length}... + + ) : ( + <> + {currentIndex + 1} / {videos.length} + {hasMore && +} + + )} + +
+ + {/* Video Feed */} +
+ {videos.map((video, index) => ( +
+ {Math.abs(index - currentIndex) <= 1 ? ( + searchByUsername(author)} + isMuted={isMuted} + onMuteToggle={() => setIsMuted(prev => !prev)} + onPauseChange={(paused) => { + setShowHeader(paused); // Show top bar when video is paused + }} + /> + ) : ( + /* Lightweight Placeholder */ +
+ {video.thumbnail ? ( + <> + +
+
+
+ + ) : ( +
+ )}
- ))} + )}
+ ))} +
+
+ + {/* FOLLOWING TAB - Minimal Style */} +
+ + {/* Header for Following Feed (if active) */} + {followingVideos.length > 0 && ( +
+

Following

+
)} - {/* Trending - 2 columns */} -
-

Trending

-
- {SUGGESTED_CATEGORIES.map(cat => ( - + {/* Case 1: Video Feed (if videos exist) */} + {followingVideos.length > 0 ? ( +
+ {followingVideos.map((video, index) => ( +
+ searchByUsername(author)} + isMuted={isMuted} + onMuteToggle={() => setIsMuted(prev => !prev)} + onPauseChange={(paused) => setShowHeader(paused)} + /> +
))}
-
- - {/* Suggested Accounts - Compact avatars */} -
-

Suggested

- - {loadingProfiles && ( -
-
-
- )} - -
- {(suggestedProfiles.length > 0 ? suggestedProfiles : SUGGESTED_ACCOUNTS.map(a => ({ username: a.username.replace('@', ''), nickname: a.label }))).slice(0, suggestedLimit).map((profile: UserProfile | { username: string; nickname: string }) => { - const username = 'username' in profile ? profile.username : ''; - - return ( - - ); - })} -
- - {/* Load More Button */} - {suggestedLimit < SUGGESTED_ACCOUNTS.length && ( - - )} -
-
-
- - {/* SEARCH TAB */} -
-
- {/* Minimal Search Input */} -
- setSearchInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - placeholder="Search..." - className="w-full bg-transparent border-b-2 border-white/20 focus:border-white/60 px-0 py-4 text-white text-lg focus:outline-none transition-colors placeholder:text-white/30" - disabled={isSearching} - /> - -

@username ยท video link ยท keyword

-
- - {/* Loading Animation - Skeleton Grid with Timer */} - {isSearching && ( -
- {/* Loading Timer Display */} -
-
- - Loading videos... - - - {Math.floor(loadingElapsed / 60)}:{(loadingElapsed % 60).toString().padStart(2, '0')} - -
-
- {[...Array(12)].map((_, i) => ( -
- ))} -
-
- )} - - {/* Empty State / Suggestions */} - {!isSearching && searchResults.length === 0 && ( - <> - {/* Trending */} -
-

Trending

-
- {SUGGESTED_CATEGORIES.map(cat => ( - - ))} -
-
- - )} - - {/* Matched User Profile Card */} - {searchMatchedUser && ( -
-
- {/* Avatar */} - {searchMatchedUser.avatar ? ( - {searchMatchedUser.username} - ) : ( -
- {searchMatchedUser.username.charAt(0).toUpperCase()} + ) : ( + /* Case 2: Management UI (if empty state) */ +
+
+ {/* Loading Spinner if Fetching */} + {isFollowingLoading && ( +
+
)} - {/* User Info */} -
-

- @{searchMatchedUser.username} - {searchMatchedUser.verified && ( - - - - )} -

- {searchMatchedUser.nickname && ( -

{searchMatchedUser.nickname}

- )} -
- {searchMatchedUser.followers !== undefined && ( - - {searchMatchedUser.followers >= 1000000 - ? `${(searchMatchedUser.followers / 1000000).toFixed(1)}M` - : searchMatchedUser.followers >= 1000 - ? `${(searchMatchedUser.followers / 1000).toFixed(0)}K` - : searchMatchedUser.followers} followers - - )} - {searchMatchedUser.likes !== undefined && ( - - {searchMatchedUser.likes >= 1000000 - ? `${(searchMatchedUser.likes / 1000000).toFixed(1)}M` - : searchMatchedUser.likes >= 1000 - ? `${(searchMatchedUser.likes / 1000).toFixed(0)}K` - : searchMatchedUser.likes} likes - - )} -
-
- - {/* View Profile Button */} - -
-
- )} - - {/* Search Results */} - {searchResults.length > 0 && ( -
-
- {searchResults.length} videos -
- {searchInput.startsWith('@') && ( - - )} + {/* Minimal Add Input */} +
+ setNewFollowInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAddFollow()} + placeholder="Add @username to follow..." + className="w-full bg-transparent border-b-2 border-white/20 focus:border-white/60 px-0 py-4 text-white text-lg focus:outline-none transition-colors placeholder:text-white/30" + />
-
-
- {searchResults.map((video) => ( -
{ - if (!video.url) return; - setOriginalVideos(videos); - setOriginalIndex(currentIndex); - const playableVideos = searchResults.filter(v => v.url); - setVideos(playableVideos); - const newIndex = playableVideos.findIndex(v => v.id === video.id); - setCurrentIndex(newIndex >= 0 ? newIndex : 0); - setIsInSearchPlayback(true); - setActiveTab('foryou'); - }} - > - {video.thumbnail ? ( - {video.author} - ) : ( -
-
-
- )} -
-

@{video.author}

+ {/* My Following - Minimal chips */} + {following.length > 0 && ( +
+

Following ({following.length})

+
+ {following.map(user => ( +
+ + +
+ ))}
-
- ))} -
-
- )} -
-
- - {/* In-Search Back Button */} - {isInSearchPlayback && ( - - )} - - {/* Profile View Overlay */} - {profileViewUsername && ( -
- {/* Profile Header */} -
-
-
- {/* Back Button */} - - - {/* Avatar */} - {profileUserData?.avatar ? ( - {profileViewUsername} - ) : ( -
- {profileViewUsername.charAt(0).toUpperCase()} +
)} - {/* User Info */} -
-

- @{profileViewUsername} -

- {profileUserData?.nickname && ( -

{profileUserData.nickname}

- )} -
- {profileUserData?.followers !== undefined && ( - - {profileUserData.followers >= 1000000 - ? `${(profileUserData.followers / 1000000).toFixed(1)}M` - : profileUserData.followers >= 1000 - ? `${(profileUserData.followers / 1000).toFixed(0)}K` - : profileUserData.followers} followers - - )} - {profileVideos.length > 0 && ( - - {profileVideos.length} videos - - )} -
-
- - {/* Follow Button */} - -
-
-
- - {/* Video Grid */} -
-
- {/* Loading Skeleton (Initial Load) with Timer */} - {profileLoading && profileVideos.length === 0 && ( -
- {/* Loading Timer Display */} -
-
- - Loading profile... - - - {Math.floor(loadingElapsed / 60)}:{(loadingElapsed % 60).toString().padStart(2, '0')} - -
-
- {[...Array(12)].map((_, i) => ( -
+ {/* Trending - 2 columns */} +
+

Trending

+
+ {SUGGESTED_CATEGORIES.map(cat => ( + ))}
- )} - {/* Video Grid */} - {profileVideos.length > 0 && ( -
- {profileVideos.map((video) => ( -
+

Suggested

+ + {loadingProfiles && ( +
+
+
+ )} + +
+ {(suggestedProfiles.length > 0 ? suggestedProfiles : SUGGESTED_ACCOUNTS.map(a => ({ username: a.username.replace('@', ''), nickname: a.label }))).slice(0, suggestedLimit).map((profile: UserProfile | { username: string; nickname: string }) => { + const username = 'username' in profile ? profile.username : ''; + + return ( + + ); + })} +
+ + {/* Load More Button */} + {suggestedLimit < SUGGESTED_ACCOUNTS.length && ( + + )} +
+
+
+ )} +
+ + {/* SEARCH TAB */} +
+
+ {/* Minimal Search Input */} +
+ {/* Close / Back Button */} + + +
+ setSearchInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + placeholder="Search..." + className="w-full bg-transparent border-b-2 border-white/20 focus:border-white/60 pl-0 pr-20 py-4 text-white text-lg focus:outline-none transition-colors placeholder:text-white/30" + disabled={isSearching} + /> + +
+ {/* Clear Button */} + {searchInput && !isSearching && ( + + )} + + {/* Search / Loading Button */} + +
+
+
+

@username ยท video link ยท keyword

+ + {/* Loading Spinner - Simple, shown alongside results */} + + + {/* Empty State / Suggestions */} + {!isSearching && searchResults.length === 0 && ( + <> + {/* Trending */} +
+

Trending

+
+ {SUGGESTED_CATEGORIES.map(cat => ( + + ))} +
+
+ + )} + + {/* Search Results */} + {searchResults.length > 0 && ( +
+
+ {searchResults.length} videos +
+ {searchInput.startsWith('@') && ( + + )} + +
+
+ + + + {/* ... inside component ... */} + + + {/* Strict Render-Time Deduplication to prevent key warnings */} + {Array.from(new Map(searchResults.map(v => [v.id, v])).values()).map((video) => ( + { if (!video.url) return; - // Save current state for back navigation setOriginalVideos(videos); setOriginalIndex(currentIndex); - // Set videos to profile videos and play - const playableVideos = profileVideos.filter(v => v.url); + const playableVideos = searchResults.filter(v => v.url); setVideos(playableVideos); const newIndex = playableVideos.findIndex(v => v.id === video.id); setCurrentIndex(newIndex >= 0 ? newIndex : 0); setIsInSearchPlayback(true); setActiveTab('foryou'); - closeProfileView(); }} > {video.thumbnail ? ( {video.description ) : (
-
+
)} - {/* Hover Overlay */} -
- - - +
+

@{video.author}

- {/* Views Badge */} - {video.views && ( -
- {video.views >= 1000000 - ? `${(video.views / 1000000).toFixed(1)}M` - : video.views >= 1000 - ? `${(video.views / 1000).toFixed(0)}K` - : video.views} -
- )} -
+ ))} -
- )} +
+
+ )} - {/* Loading More Indicator */} - {profileLoading && profileVideos.length > 0 && ( -
-
-
- )} - - {/* No More Videos */} - {!profileHasMore && profileVideos.length > 0 && ( -

No more videos

- )} - - {/* Empty State */} - {!profileLoading && profileVideos.length === 0 && ( -
-
- - - - -
-

No videos found

-
- )} -
+ {/* Loading Skeleton */} + {isSearching && ( +
+ 0 ? 3 : 12} + estimatedTime={searchResults.length === 0 ? Math.max(0, 15 - loadingElapsed) : undefined} + /> +
+ )}
- )} -
+ + {/* In-Search Back Button */} + {isInSearchPlayback && ( + + )} + + {/* Profile View Overlay */} + +
+
); }; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..3666653 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Home, Search, Heart, LogOut } from 'lucide-react'; + +interface SidebarProps { + activeTab: 'foryou' | 'search' | 'profile'; + onTabChange: (tab: 'foryou' | 'search' | 'profile') => void; + onLogout?: () => void; +} + +export const Sidebar: React.FC = ({ activeTab, onTabChange, onLogout }) => { + return ( +
+ {/* Logo */} +
+
+

+ PureStream +

+
+ + {/* Nav Items */} +
+ } + label="For You" + isActive={activeTab === 'foryou'} + onClick={() => onTabChange('foryou')} + /> + } + label="Search" + isActive={activeTab === 'search'} + onClick={() => onTabChange('search')} + /> + {/* Placeholder for future features */} + } + label="Likes" + isActive={false} + onClick={() => { }} + /> +
+ + {/* Bottom Actions */} +
+ +
+
+ ); +}; + +interface NavItemProps { + icon: React.ReactNode; + label: string; + isActive: boolean; + onClick: () => void; +} + +const NavItem: React.FC = ({ icon, label, isActive, onClick }) => { + return ( + + ); +}; diff --git a/frontend/src/components/SkeletonFeed.tsx b/frontend/src/components/SkeletonFeed.tsx new file mode 100644 index 0000000..a794b81 --- /dev/null +++ b/frontend/src/components/SkeletonFeed.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +export const SkeletonFeed: React.FC = () => { + return ( +
+ {/* Main Video Area Skeleton */} +
+
+
+ + {/* Right Sidebar Action Buttons */} +
+ {[1, 2, 3, 4].map((_, i) => ( +
+
+
+
+ ))} +
+ + {/* Bottom Info Area */} +
+
+
+
+ + {/* Music Skeleton */} +
+
+
+
+
+ + {/* Overlay Gradient */} +
+
+ ); +}; diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx new file mode 100644 index 0000000..551d50e --- /dev/null +++ b/frontend/src/components/UserCard.tsx @@ -0,0 +1,61 @@ + +import React, { useState } from 'react'; +import type { UserProfile } from '../types'; + +interface UserCardProps { + user: UserProfile; +} + +const UserCard: React.FC = ({ user }) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+
+
+ {user.username} +
+

{user.nickname}

+

@{user.username}

+
+
+ +
+ {isExpanded && ( +
+
+
+

{user.followers?.toLocaleString()}

+

Followers

+
+
+

{user.following?.toLocaleString()}

+

Following

+
+
+

{user.likes?.toLocaleString()}

+

Likes

+
+
+ {user.bio && ( +
+

Bio

+

{user.bio}

+
+ )} +
+ )} +
+ ); +}; + +export default UserCard; diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx index 8cb1578..e505128 100644 --- a/frontend/src/components/VideoPlayer.tsx +++ b/frontend/src/components/VideoPlayer.tsx @@ -4,12 +4,7 @@ import type { Video } from '../types'; import { API_BASE_URL } from '../config'; import { videoCache } from '../utils/videoCache'; -// Check if browser supports HEVC codec (Safari, Chrome 107+, Edge) -const supportsHEVC = (): boolean => { - if (typeof MediaSource === 'undefined') return false; - return MediaSource.isTypeSupported('video/mp4; codecs="hvc1"') || - MediaSource.isTypeSupported('video/mp4; codecs="hev1"'); -}; + interface HeartParticle { id: number; @@ -43,7 +38,7 @@ export const VideoPlayer: React.FC = ({ const progressBarRef = useRef(null); const [isPaused, setIsPaused] = useState(false); const [showControls, setShowControls] = useState(false); - const [objectFit] = useState<'cover' | 'contain'>('cover'); + const [progress, setProgress] = useState(0); const [duration, setDuration] = useState(0); const [isSeeking, setIsSeeking] = useState(false); @@ -55,7 +50,7 @@ export const VideoPlayer: React.FC = ({ const [cachedUrl, setCachedUrl] = useState(null); const [codecError, setCodecError] = useState(false); // True if video codec not supported const lastTapRef = useRef(0); - const browserSupportsHEVC = useRef(supportsHEVC()); + const fullProxyUrl = `${API_BASE_URL}/feed/proxy?url=${encodeURIComponent(video.url)}`; const thinProxyUrl = video.cdn_url ? `${API_BASE_URL}/feed/thin-proxy?cdn_url=${encodeURIComponent(video.cdn_url)}` : null; @@ -121,18 +116,16 @@ export const VideoPlayer: React.FC = ({ setUseFallback(false); setIsLoading(true); // Show loading for new video setCodecError(false); // Reset codec error for new video - setCachedUrl(null); - setShowSidebar(false); // Reset sidebar for new video - + // Disable client-side caching for now as it causes partial content issues with Range requests + // The backend has its own LRU cache which is sufficient const checkCache = async () => { - const cached = await videoCache.get(video.url); - if (cached) { - const blob_url = URL.createObjectURL(cached); - setCachedUrl(blob_url); - } + // Force clear any existing cache for this video to ensure we don't serve bad blobs + await videoCache.delete(video.url); + setCachedUrl(null); }; - checkCache(); + + }, [video.id]); // Progress tracking @@ -154,15 +147,21 @@ export const VideoPlayer: React.FC = ({ const videoEl = e.target as HTMLVideoElement; const error = videoEl?.error; - // Check if this is a codec/decode error (MEDIA_ERR_DECODE = 3) + // Check if this is a codec/decode error (MEDIA_ERR_DECODE = 3, MEDIA_ERR_SRC_NOT_SUPPORTED = 4) if (error?.code === 3 || error?.code === 4) { console.log(`Codec error detected (code ${error.code}):`, error.message); - // Only show codec error if browser doesn't support HEVC - if (!browserSupportsHEVC.current) { - setCodecError(true); - setIsLoading(false); + + // Always fall back to full proxy which will transcode to H.264 + if (!useFallback) { + console.log('Codec not supported, falling back to full proxy (will transcode to H.264)...'); + setUseFallback(true); return; } + + // If even full proxy failed, show error + setCodecError(true); + setIsLoading(false); + return; } if (thinProxyUrl && !useFallback) { @@ -182,25 +181,26 @@ export const VideoPlayer: React.FC = ({ }; }, [thinProxyUrl, useFallback, cachedUrl]); - useEffect(() => { - const cacheVideo = async () => { - if (!cachedUrl || !proxyUrl || proxyUrl === cachedUrl) return; - - try { - const response = await fetch(proxyUrl); - if (response.ok) { - const blob = await response.blob(); - await videoCache.set(video.url, blob); - } - } catch (error) { - console.debug('Failed to cache video:', error); - } - }; - - if (isActive && !isLoading) { - cacheVideo(); - } - }, [isActive, isLoading, proxyUrl, cachedUrl, video.url]); + // Disable active caching + // useEffect(() => { + // const cacheVideo = async () => { + // if (!cachedUrl || !proxyUrl || proxyUrl === cachedUrl) return; + // + // try { + // const response = await fetch(proxyUrl); + // if (response.ok) { + // const blob = await response.blob(); + // await videoCache.set(video.url, blob); + // } + // } catch (error) { + // console.debug('Failed to cache video:', error); + // } + // }; + // + // if (isActive && !isLoading) { + // cacheVideo(); + // } + // }, [isActive, isLoading, proxyUrl, cachedUrl, video.url]); const togglePlayPause = () => { if (!videoRef.current) return; @@ -376,20 +376,32 @@ export const VideoPlayer: React.FC = ({ onClick={handleVideoClick} onTouchStart={handleTouchStart} > - {/* Video Element - preload="metadata" for instant player readiness */} -
- {/* Side Controls - Only show when video is paused */} + {/* Side Controls - Always visible on hover or when paused */}
{/* Follow Button */} @@ -505,6 +517,7 @@ export const VideoPlayer: React.FC = ({ e.stopPropagation()} className="w-12 h-12 flex items-center justify-center bg-white/10 hover:bg-white/20 backdrop-blur-xl border border-white/10 rounded-full text-white transition-all" title="Download" > diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 38c325c..2b5fef4 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,3 +1 @@ -export const API_BASE_URL = import.meta.env.PROD - ? '/api' - : (import.meta.env.VITE_API_URL || 'http://localhost:8002/api'); +export const API_BASE_URL = '/api'; diff --git a/frontend/src/index.css b/frontend/src/index.css index d5e0c71..46be7b4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -15,6 +15,30 @@ height: 100vh; height: 100dvh; } + + body { + @apply bg-[#0f0f15] text-white antialiased; + color-scheme: dark; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + } +} + +@layer components { + .glass-panel { + @apply bg-white/5 backdrop-blur-xl border border-white/10; + } + + .glass-panel-hover { + @apply hover:bg-white/10 transition-colors duration-200; + } + + .btn-primary { + @apply bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-medium px-4 py-2 rounded-xl transition-all active:scale-95 shadow-lg shadow-indigo-500/20; + } + + .btn-ghost { + @apply hover:bg-white/10 text-gray-300 hover:text-white px-4 py-2 rounded-xl transition-all active:scale-95; + } } @layer utilities { @@ -33,33 +57,17 @@ display: none; /* Chrome, Safari and Opera */ } -} - -@layer utilities { - .scrollbar-hide::-webkit-scrollbar { - display: none; - } - - .scrollbar-hide { - -ms-overflow-style: none; - scrollbar-width: none; + + .text-shadow { + text-shadow: 0 2px 4px rgba(0,0,0,0.5); } } +/* Animations */ @keyframes shake { - - 0%, - 100% { - transform: translateX(0); - } - - 25% { - transform: translateX(-4px); - } - - 75% { - transform: translateX(4px); - } + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } } .animate-shake { @@ -67,34 +75,25 @@ } @keyframes heart-float { - 0% { - opacity: 1; - transform: scale(0) rotate(-15deg); - } - - 25% { - opacity: 1; - transform: scale(1.2) rotate(10deg); - } - - 50% { - opacity: 0.8; - transform: scale(1) translateY(-30px) rotate(-5deg); - } - - 100% { - opacity: 0; - transform: scale(0.6) translateY(-80px) rotate(15deg); - } + 0% { opacity: 1; transform: scale(0) rotate(-15deg); } + 25% { opacity: 1; transform: scale(1.2) rotate(10deg); } + 50% { opacity: 0.8; transform: scale(1) translateY(-30px) rotate(-5deg); } + 100% { opacity: 0; transform: scale(0.6) translateY(-80px) rotate(15deg); } } .animate-heart-float { animation: heart-float 1s ease-out forwards; } -body { - @apply bg-black antialiased; - color-scheme: dark; +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton-pulse { + background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; } .artplayer-app { diff --git a/frontend/src/utils/feedLoader.ts b/frontend/src/utils/feedLoader.ts index b04c106..4a780a9 100644 --- a/frontend/src/utils/feedLoader.ts +++ b/frontend/src/utils/feedLoader.ts @@ -33,7 +33,7 @@ class FeedLoader { } const cacheKey = 'feed-full'; - + // Skip cache check when explicitly requested (for infinite scroll) if (!skipCache) { const cached = this.getCached(cacheKey); @@ -43,8 +43,8 @@ class FeedLoader { } } - const videos = await this.fetchFeed(skipCache); - + const videos = await this.fetchFeed(skipCache, fast); + // Only cache if not skipping (initial load) if (!skipCache) { this.setCached(cacheKey, videos); @@ -62,11 +62,15 @@ class FeedLoader { } } - private async fetchFeed(skipCache: boolean = false): Promise { + private async fetchFeed(skipCache: boolean = false, fast: boolean = false): Promise { // Add skip_cache parameter to force backend to fetch fresh videos - const url = skipCache - ? `${API_BASE_URL}/feed?skip_cache=true` - : `${API_BASE_URL}/feed`; + let url = `${API_BASE_URL}/feed?`; + if (skipCache) url += 'skip_cache=true&'; + if (fast) url += 'fast=true&'; + + // Clean trailing & or ? + url = url.replace(/[?&]$/, ''); + const response = await axios.get(url); if (!Array.isArray(response.data)) { diff --git a/frontend/src/utils/videoPrefetch.ts b/frontend/src/utils/videoPrefetch.ts index 50c545f..415c95f 100644 --- a/frontend/src/utils/videoPrefetch.ts +++ b/frontend/src/utils/videoPrefetch.ts @@ -85,7 +85,8 @@ class VideoPrefetcher { return; } - const API_BASE_URL = 'http://localhost:8002/api'; // Hardcoded or imported from config + const API_BASE_URL_CONFIG = (await import('../config')).API_BASE_URL; + const API_BASE_URL = API_BASE_URL_CONFIG || 'http://localhost:8002/api'; // Fallback if import fails const fullProxyUrl = `${API_BASE_URL}/feed/proxy?url=${encodeURIComponent(video.url)}`; // 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; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0f57b..605b3f0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,18 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server: { + host: '0.0.0.0', // Allow access from outside the container + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8002', + changeOrigin: true, + timeout: 60000, + }, + }, + }, }) diff --git a/manage_app.ps1 b/manage_app.ps1 new file mode 100644 index 0000000..1ae312a --- /dev/null +++ b/manage_app.ps1 @@ -0,0 +1,60 @@ +param ( + [string]$Action = "start" +) + +$BackendPort = 8002 +$FrontendPort = 5173 +$RootPath = Get-Location +$BackendDir = Join-Path $RootPath "backend" +$FrontendDir = Join-Path $RootPath "frontend" + +function Stop-App { + Write-Host "Stopping PureStream..." -ForegroundColor Yellow + $ports = @($BackendPort, $FrontendPort) + foreach ($port in $ports) { + $processes = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique + if ($processes) { + foreach ($pidVal in $processes) { + Write-Host "Killing process on port $port (PID: $pidVal)" -ForegroundColor Red + Stop-Process -Id $pidVal -Force -ErrorAction SilentlyContinue + } + } else { + Write-Host "No process found on port $port" -ForegroundColor Gray + } + } + Write-Host "Stopped." -ForegroundColor Green +} + +function Start-App { + # Check if ports are already in use + $backendActive = Get-NetTCPConnection -LocalPort $BackendPort -ErrorAction SilentlyContinue + $frontendActive = Get-NetTCPConnection -LocalPort $FrontendPort -ErrorAction SilentlyContinue + + if ($backendActive -or $frontendActive) { + Write-Host "Ports are already in use. Stopping existing instances..." -ForegroundColor Yellow + Stop-App + } + + Write-Host "Starting PureStream Backend..." -ForegroundColor Cyan + # Launch in a new CMD window so user can see logs and it stays open (/k) + Start-Process "cmd.exe" -ArgumentList "/k title PureStream Backend & cd /d `"$BackendDir`" & `"$RootPath\.venv\Scripts\python.exe`" run_windows.py" -WindowStyle Normal + + Write-Host "Starting PureStream Frontend..." -ForegroundColor Cyan + # Launch in a new CMD window + Start-Process "cmd.exe" -ArgumentList "/k title PureStream Frontend & cd /d `"$FrontendDir`" & npm run dev" -WindowStyle Normal + + Write-Host "PureStream is starting!" -ForegroundColor Green + Write-Host "Backend API: http://localhost:$BackendPort" + Write-Host "Frontend UI: http://localhost:$FrontendPort" +} + +switch ($Action.ToLower()) { + "stop" { Stop-App } + "start" { Start-App } + "restart" { Stop-App; Start-App } + default { + Write-Host "Usage: .\manage_app.ps1 [start|stop|restart]" -ForegroundColor Red + Write-Host "Defaulting to 'start'..." -ForegroundColor Yellow + Start-App + } +} diff --git a/restart_app.sh b/restart_app.sh new file mode 100644 index 0000000..c02c509 --- /dev/null +++ b/restart_app.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +echo "๐Ÿ”„ Restarting PureStream WebApp..." + +# Function to kill process on port +kill_port() { + PORT=$1 + if lsof -i:$PORT -t >/dev/null; then + PID=$(lsof -ti:$PORT) + echo "Killing process on port $PORT (PID: $PID)..." + kill -9 $PID + else + echo "Port $PORT is free." + fi +} + +# 1. Stop existing processes +echo "๐Ÿ›‘ Stopping services..." +kill_port 8000 # Backend +kill_port 8002 # Frontend (Target) +kill_port 8003 # Frontend (Alt) +kill_port 5173 # Frontend (Default) + +# 2. Start Backend +echo "๐Ÿš€ Starting Backend (Port 8000)..." +cd backend +# Check if venv exists matching user env, else use python3 +PYTHON_CMD="python3" +# Start uvicorn in background +nohup $PYTHON_CMD -m uvicorn main:app --reload --host 0.0.0.0 --port 8000 > ../backend.log 2>&1 & +BACKEND_PID=$! +echo "Backend started with PID $BACKEND_PID" +cd .. + +# 3. Start Frontend +echo "๐ŸŽจ Starting Frontend (Port 8002)..." +cd frontend +# Start vite in background +nohup npm run dev -- --port 8002 --host > ../frontend.log 2>&1 & +FRONTEND_PID=$! +echo "Frontend started with PID $FRONTEND_PID" +cd .. + +echo "โœ… App restarted successfully!" +echo "--------------------------------" +echo "Backend: http://localhost:8000" +echo "Frontend: http://localhost:8002" +echo "--------------------------------" +echo "Logs are being written to backend.log and frontend.log" diff --git a/run_debug_search.ps1 b/run_debug_search.ps1 new file mode 100644 index 0000000..05f6132 --- /dev/null +++ b/run_debug_search.ps1 @@ -0,0 +1,2 @@ +$env:PYTHONPATH = "c:\Users\Admin\Downloads\kv-tiktok\backend" +& "c:\Users\Admin\Downloads\kv-tiktok\.venv\Scripts\python.exe" tests/debug_search.py diff --git a/start_app.bat b/start_app.bat new file mode 100644 index 0000000..fcf4231 --- /dev/null +++ b/start_app.bat @@ -0,0 +1,3 @@ +@echo off +cd /d "%~dp0" +powershell -ExecutionPolicy Bypass -File manage_app.ps1 start diff --git a/stop_app.bat b/stop_app.bat new file mode 100644 index 0000000..1f01464 --- /dev/null +++ b/stop_app.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +powershell -ExecutionPolicy Bypass -File manage_app.ps1 stop +pause diff --git a/test_stealth.py b/test_stealth.py new file mode 100644 index 0000000..e157ad4 --- /dev/null +++ b/test_stealth.py @@ -0,0 +1,12 @@ +import sys +print(f"Python: {sys.executable}") +print(f"Path: {sys.path}") +try: + import playwright_stealth + print(f"Module: {playwright_stealth}") + from playwright_stealth import stealth_async + print("Import successful!") +except ImportError as e: + print(f"Import failed: {e}") +except Exception as e: + print(f"Error: {e}") diff --git a/tests/debug_search.py b/tests/debug_search.py new file mode 100644 index 0000000..addcf3b --- /dev/null +++ b/tests/debug_search.py @@ -0,0 +1,41 @@ +import json +import urllib.request +import urllib.parse +import os +import sys + +def debug_search(): + base_url = "http://localhost:8002/api/user/search" + query = "hot trend" + params = urllib.parse.urlencode({"query": query, "limit": 10}) + url = f"{base_url}?{params}" + + print(f"Testing search for: '{query}'") + print(f"URL: {url}") + + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=60) as response: + status_code = response.getcode() + print(f"Status Code: {status_code}") + + if status_code == 200: + data = json.loads(response.read().decode('utf-8')) + print(f"Source: {data.get('source')}") + print(f"Count: {data.get('count')}") + videos = data.get("videos", []) + if not videos: + print("ERROR: No videos returned!") + else: + print(f"First video: {videos[0].get('id')} - {videos[0].get('desc', 'No desc')}") + else: + print(f"Error: Status {status_code}") + + except urllib.error.HTTPError as e: + print(f"HTTP Error: {e.code} - {e.reason}") + print(e.read().decode('utf-8')) + except Exception as e: + print(f"Request failed: {e}") + +if __name__ == "__main__": + debug_search() diff --git a/tests/inspect_html.py b/tests/inspect_html.py new file mode 100644 index 0000000..50ade8f --- /dev/null +++ b/tests/inspect_html.py @@ -0,0 +1,29 @@ +from bs4 import BeautifulSoup +import re + +with open("debug_search_page.html", "r", encoding="utf-8") as f: + html = f.read() + +soup = BeautifulSoup(html, "html.parser") + +# Inspect text occurrences +print("\n--- Searching for 'trend' text ---") +text_matches = soup.find_all(string=re.compile("trend", re.IGNORECASE)) +print(f"Found {len(text_matches)} text matches.") + +unique_parents = set() +for text in text_matches: + parent = text.parent + if parent and parent.name != "script" and parent.name != "style": + # Get up to 3 levels of parents + chain = [] + curr = parent + for _ in range(3): + if curr: + chain.append(f"<{curr.name} class='{'.'.join(curr.get('class', []))}'>") + curr = curr.parent + unique_parents.add(" -> ".join(chain)) + +for p in list(unique_parents)[:10]: + print(p) + diff --git a/tests/parse_ssr_data.py b/tests/parse_ssr_data.py new file mode 100644 index 0000000..5ce1b8d --- /dev/null +++ b/tests/parse_ssr_data.py @@ -0,0 +1,45 @@ +from bs4 import BeautifulSoup +import json + +with open("debug_search_page.html", "r", encoding="utf-8") as f: + html = f.read() + +soup = BeautifulSoup(html, "html.parser") +script = soup.find("script", id="__UNIVERSAL_DATA_FOR_REHYDRATION__") + +if script: + try: + data = json.loads(script.string) + print("Found SSR Data!") + + # Save pretty printed + with open("ssr_data.json", "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + # Search for video list + # Look in __DEFAULT_SCOPE__ -> webapp.search-video -> searchVideoList (guessing keys) + # or just traverse and print keys + + def find_keys(obj, target_key, path=""): + if isinstance(obj, dict): + for k, v in obj.items(): + current_path = f"{path}.{k}" + if target_key.lower() in k.lower(): + print(f"Found key '{k}' at {current_path}") + find_keys(v, target_key, current_path) + elif isinstance(obj, list): + for i, item in enumerate(obj): + find_keys(item, target_key, f"{path}[{i}]") + + print("\nSearching for 'item' or 'list' keys...") + find_keys(data, "item") + find_keys(data, "list") + + # Check specific known paths + default_scope = data.get("__DEFAULT_SCOPE__", {}) + print(f"\nTop level keys: {list(default_scope.keys())}") + + except json.JSONDecodeError as e: + print(f"JSON Error: {e}") +else: + print("Script tag not found.") diff --git a/simple_test.py b/tests/simple_test.py similarity index 96% rename from simple_test.py rename to tests/simple_test.py index f988fc4..2d7b105 100644 --- a/simple_test.py +++ b/tests/simple_test.py @@ -1,30 +1,30 @@ -import urllib.request -import json - -try: - print("Testing /health...") - with urllib.request.urlopen("http://localhost:8002/health", timeout=5) as r: - print(f"Health: {r.status}") - - print("Testing /api/feed...") - with open("temp_cookies.json", "r") as f: - data = json.load(f) - - # Ensure list format - if isinstance(data, dict) and "credentials" in data: - data = data["credentials"] - - # Prepare body as dict for safety with new Union type - body = {"credentials": data} - - req = urllib.request.Request( - "http://localhost:8002/api/feed", - data=json.dumps(body).encode('utf-8'), - headers={'Content-Type': 'application/json'} - ) - with urllib.request.urlopen(req, timeout=30) as r: - print(f"Feed: {r.status}") - print(r.read().decode('utf-8')[:100]) - -except Exception as e: - print(f"Error: {e}") +import urllib.request +import json + +try: + print("Testing /health...") + with urllib.request.urlopen("http://localhost:8002/health", timeout=5) as r: + print(f"Health: {r.status}") + + print("Testing /api/feed...") + with open("temp_cookies.json", "r") as f: + data = json.load(f) + + # Ensure list format + if isinstance(data, dict) and "credentials" in data: + data = data["credentials"] + + # Prepare body as dict for safety with new Union type + body = {"credentials": data} + + req = urllib.request.Request( + "http://localhost:8002/api/feed", + data=json.dumps(body).encode('utf-8'), + headers={'Content-Type': 'application/json'} + ) + with urllib.request.urlopen(req, timeout=30) as r: + print(f"Feed: {r.status}") + print(r.read().decode('utf-8')[:100]) + +except Exception as e: + print(f"Error: {e}") diff --git a/tests/test_crawl.py b/tests/test_crawl.py new file mode 100644 index 0000000..2f3ba60 --- /dev/null +++ b/tests/test_crawl.py @@ -0,0 +1,42 @@ +import asyncio +import base64 +from crawl4ai import AsyncWebCrawler + +async def main(): + print("Starting Crawl4AI test...") + async with AsyncWebCrawler(verbose=True) as crawler: + url = "https://www.tiktok.com/search?q=hot+trend" + print(f"Crawling: {url}") + + # Browser config + run_conf = { + "url": url, + "wait_for": "css:[data-e2e='search_video_item']", + "css_selector": "[data-e2e='search_video_item']", + "screenshot": True, + "magic": True + } + + print(f"Crawling with config: {run_conf}") + result = await crawler.arun(**run_conf) + + if result.success: + print("Crawl successful!") + print(f"HTML length: {len(result.html)}") + + if result.screenshot: + with open("crawl_screenshot.png", "wb") as f: + f.write(base64.b64decode(result.screenshot)) + print("Saved screenshot to crawl_screenshot.png") + + # Save for inspection + with open("crawl_debug.html", "w", encoding="utf-8") as f: + f.write(result.html) + with open("crawl_debug.md", "w", encoding="utf-8") as f: + f.write(result.markdown) + + else: + print(f"Crawl failed: {result.error_message}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/test_login.py b/tests/test_login.py similarity index 96% rename from test_login.py rename to tests/test_login.py index faff657..0f13e21 100644 --- a/test_login.py +++ b/tests/test_login.py @@ -1,16 +1,16 @@ -import requests -import time - -URL = "http://localhost:8002/api/auth/admin-login" - -def test_login(): - print("Testing Admin Login...") - try: - res = requests.post(URL, json={"password": "admin123"}) - print(f"Status: {res.status_code}") - print(f"Response: {res.text}") - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - test_login() +import requests +import time + +URL = "http://localhost:8002/api/auth/admin-login" + +def test_login(): + print("Testing Admin Login...") + try: + res = requests.post(URL, json={"password": "admin123"}) + print(f"Status: {res.status_code}") + print(f"Response: {res.text}") + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + test_login() diff --git a/test_request.py b/tests/test_request.py similarity index 96% rename from test_request.py rename to tests/test_request.py index c78e9f1..0986255 100644 --- a/test_request.py +++ b/tests/test_request.py @@ -1,30 +1,30 @@ -import urllib.request -import json -import os - -with open("temp_cookies.json", "r") as f: - data = json.load(f) - -# Ensure data is in the expected dict format for the request body -if isinstance(data, list): - # If temp_cookies is just the list, wrap it - body = {"credentials": data} -elif "credentials" not in data: - body = {"credentials": data} -else: - body = data - -req = urllib.request.Request( - "http://localhost:8002/api/feed", - data=json.dumps(body).encode('utf-8'), - headers={'Content-Type': 'application/json'} -) - -try: - with urllib.request.urlopen(req) as response: - print(response.read().decode('utf-8')) -except urllib.error.HTTPError as e: - print(f"HTTP Error: {e.code}") - print(e.read().decode('utf-8')) -except Exception as e: - print(f"Error: {e}") +import urllib.request +import json +import os + +with open("temp_cookies.json", "r") as f: + data = json.load(f) + +# Ensure data is in the expected dict format for the request body +if isinstance(data, list): + # If temp_cookies is just the list, wrap it + body = {"credentials": data} +elif "credentials" not in data: + body = {"credentials": data} +else: + body = data + +req = urllib.request.Request( + "http://localhost:8002/api/feed", + data=json.dumps(body).encode('utf-8'), + headers={'Content-Type': 'application/json'} +) + +try: + with urllib.request.urlopen(req) as response: + print(response.read().decode('utf-8')) +except urllib.error.HTTPError as e: + print(f"HTTP Error: {e.code}") + print(e.read().decode('utf-8')) +except Exception as e: + print(f"Error: {e}") diff --git a/test_search.py b/tests/test_search.py similarity index 96% rename from test_search.py rename to tests/test_search.py index 606d94e..be54400 100644 --- a/test_search.py +++ b/tests/test_search.py @@ -1,35 +1,35 @@ -import requests -import json -import time - -BASE_URL = "http://localhost:8002/api/user/search" - -def test_search(): - print("Testing Search API...") - try: - # Simple query - params = { - "query": "dance", - "limit": 50, - "cursor": 0 - } - start = time.time() - res = requests.get(BASE_URL, params=params) - duration = time.time() - start - - print(f"Status Code: {res.status_code}") - print(f"Duration: {duration:.2f}s") - - if res.status_code == 200: - data = res.json() - print(f"Videos Found: {len(data.get('videos', []))}") - # print(json.dumps(data, indent=2)) - else: - print("Error Response:") - print(res.text) - - except Exception as e: - print(f"Request Failed: {e}") - -if __name__ == "__main__": - test_search() +import requests +import json +import time + +BASE_URL = "http://localhost:8002/api/user/search" + +def test_search(): + print("Testing Search API...") + try: + # Simple query + params = { + "query": "dance", + "limit": 50, + "cursor": 0 + } + start = time.time() + res = requests.get(BASE_URL, params=params) + duration = time.time() - start + + print(f"Status Code: {res.status_code}") + print(f"Duration: {duration:.2f}s") + + if res.status_code == 200: + data = res.json() + print(f"Videos Found: {len(data.get('videos', []))}") + # print(json.dumps(data, indent=2)) + else: + print("Error Response:") + print(res.text) + + except Exception as e: + print(f"Request Failed: {e}") + +if __name__ == "__main__": + test_search()