Files
tasteby/backend/api/routes/channels.py
joungmin 3694730501 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>
2026-03-07 14:52:20 +09:00

90 lines
2.7 KiB
Python

"""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("")
def list_channels():
return youtube.get_active_channels()
@router.post("", status_code=201)
def create_channel(body: ChannelCreate):
try:
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():
raise HTTPException(409, "Channel already exists")
raise
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:
return None
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}