Add admin features, responsive UI, user reviews, visit stats, and channel-colored markers
- Admin: video management with Google Maps match status, manual restaurant mapping, restaurant remap on name change - Admin: user management tab with favorites/reviews detail - Admin: channel deletion fix for IDs with slashes - Frontend: responsive mobile layout (map top, list bottom, 2-row header) - Frontend: channel-colored map markers with legend - Frontend: my reviews list, favorites toggle, visit counter overlay - Frontend: force light mode for dark theme devices - Backend: visit tracking (site_visits table), user reviews endpoint - Backend: bulk transcript/extract streaming, geocode key fixes - Nginx config for production deployment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,24 @@
|
||||
"""Channel API routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core import youtube
|
||||
|
||||
_executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ChannelCreate(BaseModel):
|
||||
channel_id: str
|
||||
channel_name: str
|
||||
title_filter: str | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
@@ -21,7 +29,7 @@ def list_channels():
|
||||
@router.post("", status_code=201)
|
||||
def create_channel(body: ChannelCreate):
|
||||
try:
|
||||
row_id = youtube.add_channel(body.channel_id, body.channel_name)
|
||||
row_id = youtube.add_channel(body.channel_id, body.channel_name, body.title_filter)
|
||||
return {"id": row_id, "channel_id": body.channel_id}
|
||||
except Exception as e:
|
||||
if "UQ_CHANNELS_CID" in str(e).upper():
|
||||
@@ -29,18 +37,53 @@ def create_channel(body: ChannelCreate):
|
||||
raise
|
||||
|
||||
|
||||
@router.post("/{channel_id}/scan")
|
||||
def scan_channel(channel_id: str):
|
||||
"""Trigger a scan for new videos from this channel."""
|
||||
def _do_scan(channel_id: str, full: bool):
|
||||
"""Sync scan logic, runs in thread pool."""
|
||||
channels = youtube.get_active_channels()
|
||||
ch = next((c for c in channels if c["channel_id"] == channel_id), None)
|
||||
if not ch:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
return None
|
||||
|
||||
videos = youtube.fetch_channel_videos(channel_id, max_results=50)
|
||||
new_count = 0
|
||||
for v in videos:
|
||||
row_id = youtube.save_video(ch["id"], v)
|
||||
if row_id:
|
||||
new_count += 1
|
||||
return {"total_fetched": len(videos), "new_videos": new_count}
|
||||
after = None if full else youtube.get_latest_video_date(ch["id"])
|
||||
title_filter = ch.get("title_filter")
|
||||
existing_vids = youtube.get_existing_video_ids(ch["id"])
|
||||
|
||||
candidates = []
|
||||
total_fetched = 0
|
||||
for videos_page in youtube.fetch_channel_videos_iter(channel_id, published_after=after):
|
||||
total_fetched += len(videos_page)
|
||||
new_in_page = 0
|
||||
for v in videos_page:
|
||||
if title_filter and title_filter not in v["title"]:
|
||||
continue
|
||||
if v["video_id"] in existing_vids:
|
||||
continue
|
||||
candidates.append(v)
|
||||
new_in_page += 1
|
||||
if not full and new_in_page == 0 and total_fetched > 50:
|
||||
break
|
||||
|
||||
new_count = youtube.save_videos_batch(ch["id"], candidates)
|
||||
return {"total_fetched": total_fetched, "new_videos": new_count}
|
||||
|
||||
|
||||
@router.post("/{channel_id}/scan")
|
||||
async def scan_channel(channel_id: str, full: bool = False):
|
||||
"""Trigger a scan for new videos from this channel (non-blocking)."""
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(_executor, _do_scan, channel_id, full)
|
||||
if result is None:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/{channel_id:path}")
|
||||
def delete_channel(channel_id: str):
|
||||
"""Deactivate a channel. Accepts channel_id or DB id."""
|
||||
deleted = youtube.deactivate_channel(channel_id)
|
||||
if not deleted:
|
||||
# Try by DB id
|
||||
deleted = youtube.deactivate_channel_by_db_id(channel_id)
|
||||
if not deleted:
|
||||
raise HTTPException(404, "Channel not found")
|
||||
return {"ok": True}
|
||||
|
||||
Reference in New Issue
Block a user