diff --git a/backend/app/database.py b/backend/app/database.py index 45de775..98f09c8 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -32,3 +32,13 @@ async def init_db(): await conn.execute(text( "CREATE INDEX IF NOT EXISTS ix_download_logs_downloaded_at ON download_logs (downloaded_at)" )) + # Migrate: add geo columns to existing download_logs table (idempotent) + for col_def in [ + "ALTER TABLE download_logs ADD COLUMN country_code VARCHAR(8) DEFAULT ''", + "ALTER TABLE download_logs ADD COLUMN country VARCHAR(128) DEFAULT ''", + "ALTER TABLE download_logs ADD COLUMN city VARCHAR(128) DEFAULT ''", + ]: + try: + await conn.execute(text(col_def)) + except Exception: + pass # Column already exists diff --git a/backend/app/models.py b/backend/app/models.py index 40f5e44..7990255 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -42,6 +42,9 @@ class DownloadLog(Base): user_agent = Column(Text, default="") browser = Column(String(64), default="") # Chrome / Firefox / Safari / Edge / … device = Column(String(32), default="") # desktop / mobile / tablet / bot + country_code = Column(String(8), default="") # e.g. CN + country = Column(String(128), default="") # e.g. China + city = Column(String(128), default="") # e.g. Shanghai downloaded_at = Column(DateTime, default=datetime.utcnow, index=True) video = relationship("Video", back_populates="logs") diff --git a/backend/app/routes/download.py b/backend/app/routes/download.py index 62bb031..37a89a1 100644 --- a/backend/app/routes/download.py +++ b/backend/app/routes/download.py @@ -63,18 +63,42 @@ def _client_ip(request: Request) -> str: return "" +async def _geo_lookup(ip: str) -> tuple[str, str, str]: + """Return (country_code, country, city) via ip-api.com. Falls back to empty strings.""" + if not ip or ip in ("127.0.0.1", "::1"): + return "", "", "" + try: + import httpx + async with httpx.AsyncClient(timeout=5) as client: + res = await client.get( + f"http://ip-api.com/json/{ip}", + params={"fields": "status,countryCode,country,city"}, + ) + data = res.json() + if data.get("status") == "success": + return data.get("countryCode", ""), data.get("country", ""), data.get("city", "") + except Exception as e: + logger.debug(f"Geo lookup failed for {ip}: {e}") + return "", "", "" + + async def _log_download(video_id: int, request: Request): - """Write a DownloadLog entry (fire-and-forget).""" + """Write a DownloadLog entry with geo info (fire-and-forget).""" try: ua = request.headers.get("user-agent", "") browser, device = _parse_ua(ua) + ip = _client_ip(request) + country_code, country, city = await _geo_lookup(ip) async with async_session() as db: db.add(DownloadLog( video_id=video_id, - ip=_client_ip(request), + ip=ip, user_agent=ua[:512], browser=browser, device=device, + country_code=country_code, + country=country, + city=city, downloaded_at=datetime.utcnow(), )) await db.commit() diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 507d0a2..ebb963a 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -95,6 +95,9 @@ class DownloadLogInfo(BaseModel): user_agent: str browser: str device: str + country_code: str = "" + country: str = "" + city: str = "" downloaded_at: datetime class Config: diff --git a/backend/requirements.txt b/backend/requirements.txt index 8047b62..82d72d2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,3 +8,4 @@ python-dotenv==1.0.1 python-multipart==0.0.12 yt-dlp>=2024.1.0 pydantic>=2.0.0 +httpx>=0.27.0 diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 0a437ce..b2d3746 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -66,6 +66,7 @@