304
web/backend/routers/dashboard.py
Normal file
304
web/backend/routers/dashboard.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Dashboard API Router
|
||||
|
||||
Provides endpoints for dashboard-specific data like recent items across different locations.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from typing import Dict, Any, Optional
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from ..core.dependencies import get_current_user, get_app_state
|
||||
from ..core.exceptions import handle_exceptions
|
||||
from modules.universal_logger import get_logger
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
logger = get_logger('API')
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
|
||||
|
||||
@router.get("/recent-items")
|
||||
@limiter.limit("60/minute")
|
||||
@handle_exceptions
|
||||
async def get_recent_items(
|
||||
request: Request,
|
||||
limit: int = 20,
|
||||
since_id: Optional[int] = None,
|
||||
current_user=Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get NEW items from Media, Review, and Internet Discovery for dashboard cards.
|
||||
|
||||
Uses file_inventory.id for ordering since it monotonically increases with
|
||||
insertion order. download_date from the downloads table is included for
|
||||
display but not used for ordering (batch downloads can interleave timestamps).
|
||||
|
||||
Args:
|
||||
limit: Max items per category
|
||||
since_id: Optional file_inventory ID - only return items with id > this value
|
||||
|
||||
Returns up to `limit` items from each location, sorted by most recently added first.
|
||||
"""
|
||||
app_state = get_app_state()
|
||||
with app_state.db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Media items (location='final')
|
||||
# ORDER BY fi.id DESC — id is monotonically increasing and reflects insertion order.
|
||||
# download_date is included for display but NOT used for ordering.
|
||||
if since_id:
|
||||
cursor.execute("""
|
||||
SELECT fi.id, fi.file_path, fi.filename, fi.source, fi.platform, fi.content_type,
|
||||
fi.file_size, COALESCE(d.download_date, fi.created_date) as added_at,
|
||||
fi.width, fi.height
|
||||
FROM file_inventory fi
|
||||
LEFT JOIN downloads d ON d.filename = fi.filename
|
||||
WHERE fi.location = 'final'
|
||||
AND fi.id > ?
|
||||
AND (fi.moved_from_review IS NULL OR fi.moved_from_review = 0)
|
||||
AND (fi.from_discovery IS NULL OR fi.from_discovery = 0)
|
||||
ORDER BY fi.id DESC
|
||||
LIMIT ?
|
||||
""", (since_id, limit))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT fi.id, fi.file_path, fi.filename, fi.source, fi.platform, fi.content_type,
|
||||
fi.file_size, COALESCE(d.download_date, fi.created_date) as added_at,
|
||||
fi.width, fi.height
|
||||
FROM file_inventory fi
|
||||
LEFT JOIN downloads d ON d.filename = fi.filename
|
||||
WHERE fi.location = 'final'
|
||||
AND (fi.moved_from_review IS NULL OR fi.moved_from_review = 0)
|
||||
AND (fi.from_discovery IS NULL OR fi.from_discovery = 0)
|
||||
ORDER BY fi.id DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
media_items = []
|
||||
for row in cursor.fetchall():
|
||||
media_items.append({
|
||||
'id': row[0],
|
||||
'file_path': row[1],
|
||||
'filename': row[2],
|
||||
'source': row[3],
|
||||
'platform': row[4],
|
||||
'media_type': row[5],
|
||||
'file_size': row[6],
|
||||
'added_at': row[7],
|
||||
'width': row[8],
|
||||
'height': row[9]
|
||||
})
|
||||
|
||||
# Get total count for new media items
|
||||
if since_id:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM file_inventory
|
||||
WHERE location = 'final'
|
||||
AND id > ?
|
||||
AND (moved_from_review IS NULL OR moved_from_review = 0)
|
||||
AND (from_discovery IS NULL OR from_discovery = 0)
|
||||
""", (since_id,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM file_inventory
|
||||
WHERE location = 'final'
|
||||
AND (moved_from_review IS NULL OR moved_from_review = 0)
|
||||
AND (from_discovery IS NULL OR from_discovery = 0)
|
||||
""")
|
||||
media_count = cursor.fetchone()[0]
|
||||
|
||||
# Review items (location='review')
|
||||
if since_id:
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.file_path, f.filename, f.source, f.platform, f.content_type,
|
||||
f.file_size, COALESCE(d.download_date, f.created_date) as added_at,
|
||||
f.width, f.height,
|
||||
CASE WHEN fr.id IS NOT NULL THEN 1 ELSE 0 END as face_scanned,
|
||||
fr.has_match as face_matched, fr.confidence as face_confidence, fr.matched_person
|
||||
FROM file_inventory f
|
||||
LEFT JOIN downloads d ON d.filename = f.filename
|
||||
LEFT JOIN face_recognition_scans fr ON f.file_path = fr.file_path
|
||||
WHERE f.location = 'review'
|
||||
AND f.id > ?
|
||||
AND (f.moved_from_media IS NULL OR f.moved_from_media = 0)
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
""", (since_id, limit))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.file_path, f.filename, f.source, f.platform, f.content_type,
|
||||
f.file_size, COALESCE(d.download_date, f.created_date) as added_at,
|
||||
f.width, f.height,
|
||||
CASE WHEN fr.id IS NOT NULL THEN 1 ELSE 0 END as face_scanned,
|
||||
fr.has_match as face_matched, fr.confidence as face_confidence, fr.matched_person
|
||||
FROM file_inventory f
|
||||
LEFT JOIN downloads d ON d.filename = f.filename
|
||||
LEFT JOIN face_recognition_scans fr ON f.file_path = fr.file_path
|
||||
WHERE f.location = 'review'
|
||||
AND (f.moved_from_media IS NULL OR f.moved_from_media = 0)
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
review_items = []
|
||||
for row in cursor.fetchall():
|
||||
face_recognition = None
|
||||
if row[10]: # face_scanned
|
||||
face_recognition = {
|
||||
'scanned': True,
|
||||
'matched': bool(row[11]) if row[11] is not None else False,
|
||||
'confidence': row[12],
|
||||
'matched_person': row[13]
|
||||
}
|
||||
|
||||
review_items.append({
|
||||
'id': row[0],
|
||||
'file_path': row[1],
|
||||
'filename': row[2],
|
||||
'source': row[3],
|
||||
'platform': row[4],
|
||||
'media_type': row[5],
|
||||
'file_size': row[6],
|
||||
'added_at': row[7],
|
||||
'width': row[8],
|
||||
'height': row[9],
|
||||
'face_recognition': face_recognition
|
||||
})
|
||||
|
||||
# Get total count for new review items
|
||||
if since_id:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM file_inventory
|
||||
WHERE location = 'review'
|
||||
AND id > ?
|
||||
AND (moved_from_media IS NULL OR moved_from_media = 0)
|
||||
""", (since_id,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM file_inventory
|
||||
WHERE location = 'review'
|
||||
AND (moved_from_media IS NULL OR moved_from_media = 0)
|
||||
""")
|
||||
review_count = cursor.fetchone()[0]
|
||||
|
||||
# Internet Discovery items (celebrity_discovered_videos with status='new')
|
||||
internet_discovery_items = []
|
||||
internet_discovery_count = 0
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
v.id,
|
||||
v.video_id,
|
||||
v.title,
|
||||
v.thumbnail,
|
||||
v.channel_name,
|
||||
v.platform,
|
||||
v.duration,
|
||||
v.max_resolution,
|
||||
v.status,
|
||||
v.discovered_at,
|
||||
v.url,
|
||||
v.view_count,
|
||||
v.upload_date,
|
||||
c.name as celebrity_name
|
||||
FROM celebrity_discovered_videos v
|
||||
LEFT JOIN celebrity_profiles c ON v.celebrity_id = c.id
|
||||
WHERE v.status = 'new'
|
||||
ORDER BY v.id DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
for row in cursor.fetchall():
|
||||
internet_discovery_items.append({
|
||||
'id': row[0],
|
||||
'video_id': row[1],
|
||||
'title': row[2],
|
||||
'thumbnail': row[3],
|
||||
'channel_name': row[4],
|
||||
'platform': row[5],
|
||||
'duration': row[6],
|
||||
'max_resolution': row[7],
|
||||
'status': row[8],
|
||||
'discovered_at': row[9],
|
||||
'url': row[10],
|
||||
'view_count': row[11],
|
||||
'upload_date': row[12],
|
||||
'celebrity_name': row[13]
|
||||
})
|
||||
|
||||
# Get total count for internet discovery
|
||||
cursor.execute("SELECT COUNT(*) FROM celebrity_discovered_videos WHERE status = 'new'")
|
||||
internet_discovery_count = cursor.fetchone()[0]
|
||||
except Exception as e:
|
||||
# Table might not exist if celebrity feature not used
|
||||
logger.warning(f"Could not fetch internet discovery items: {e}", module="Dashboard")
|
||||
|
||||
return {
|
||||
'media': {
|
||||
'count': media_count,
|
||||
'items': media_items
|
||||
},
|
||||
'review': {
|
||||
'count': review_count,
|
||||
'items': review_items
|
||||
},
|
||||
'internet_discovery': {
|
||||
'count': internet_discovery_count,
|
||||
'items': internet_discovery_items
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/dismissed-cards")
|
||||
@limiter.limit("60/minute")
|
||||
@handle_exceptions
|
||||
async def get_dismissed_cards(
|
||||
request: Request,
|
||||
user=Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the user's dismissed card IDs."""
|
||||
app_state = get_app_state()
|
||||
user_id = user.get('username', 'default')
|
||||
|
||||
with app_state.db.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT preference_value FROM user_preferences
|
||||
WHERE user_id = ? AND preference_key = 'dashboard_dismissed_cards'
|
||||
""", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row and row[0]:
|
||||
import json
|
||||
return json.loads(row[0])
|
||||
|
||||
return {'media': None, 'review': None, 'internet_discovery': None}
|
||||
|
||||
|
||||
@router.post("/dismissed-cards")
|
||||
@limiter.limit("30/minute")
|
||||
@handle_exceptions
|
||||
async def set_dismissed_cards(
|
||||
request: Request,
|
||||
data: Dict[str, Any],
|
||||
user=Depends(get_current_user)
|
||||
) -> Dict[str, str]:
|
||||
"""Save the user's dismissed card IDs."""
|
||||
import json
|
||||
app_state = get_app_state()
|
||||
user_id = user.get('username', 'default')
|
||||
|
||||
with app_state.db.get_connection(for_write=True) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO user_preferences (user_id, preference_key, preference_value, updated_at)
|
||||
VALUES (?, 'dashboard_dismissed_cards', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(user_id, preference_key) DO UPDATE SET
|
||||
preference_value = excluded.preference_value,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (user_id, json.dumps(data)))
|
||||
|
||||
return {'status': 'ok'}
|
||||
Reference in New Issue
Block a user