Files
haunt-fm/src/haunt_fm/api/status.py

135 lines
5.1 KiB
Python
Raw Normal View History

from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
from haunt_fm.db import get_session
from haunt_fm.models.track import (
ListenEvent,
Playlist,
TasteProfile,
Track,
TrackEmbedding,
)
from haunt_fm.config import settings
from haunt_fm.services.embedding import is_model_loaded
from haunt_fm.services.embedding_worker import is_running as is_worker_running
from haunt_fm.services.embedding_worker import last_processed as worker_last_processed
from haunt_fm.services.skip_detector import get_sessions as get_skip_sessions
from haunt_fm.services.skip_detector import is_running as is_skip_detector_running
router = APIRouter(prefix="/api")
@router.get("/status")
async def status(session: AsyncSession = Depends(get_session)):
# DB connectivity
try:
await session.execute(text("SELECT 1"))
db_connected = True
except Exception:
db_connected = False
if not db_connected:
return {"healthy": False, "db_connected": False}
now = datetime.now(timezone.utc)
day_ago = now - timedelta(days=1)
# Listen events
total_events = (await session.execute(select(func.count(ListenEvent.id)))).scalar() or 0
events_24h = (
await session.execute(
select(func.count(ListenEvent.id)).where(ListenEvent.listened_at >= day_ago)
)
).scalar() or 0
latest_event = (
await session.execute(select(func.max(ListenEvent.listened_at)))
).scalar()
# Tracks
total_tracks = (await session.execute(select(func.count(Track.id)))).scalar() or 0
from_history = (
await session.execute(
select(func.count(func.distinct(ListenEvent.track_id)))
)
).scalar() or 0
from_discovery = total_tracks - from_history
# Embeddings
def _embedding_count(status_val: str):
return select(func.count(Track.id)).where(Track.embedding_status == status_val)
emb_done = (await session.execute(_embedding_count("done"))).scalar() or 0
emb_pending = (await session.execute(_embedding_count("pending"))).scalar() or 0
emb_failed = (await session.execute(_embedding_count("failed"))).scalar() or 0
emb_no_preview = (await session.execute(_embedding_count("no_preview"))).scalar() or 0
# Taste profile
taste = (await session.execute(select(TasteProfile).where(TasteProfile.name == "default"))).scalar()
# Playlists
total_playlists = (await session.execute(select(func.count(Playlist.id)))).scalar() or 0
last_playlist = (await session.execute(select(func.max(Playlist.created_at)))).scalar()
return {
"healthy": db_connected,
"db_connected": db_connected,
"clap_model_loaded": is_model_loaded(),
"pipeline": {
"listen_events": {
"total": total_events,
"last_24h": events_24h,
"latest": latest_event.isoformat() if latest_event else None,
},
"tracks": {
"total": total_tracks,
"from_history": from_history,
"from_discovery": from_discovery,
},
"embeddings": {
"done": emb_done,
"pending": emb_pending,
"failed": emb_failed,
"no_preview": emb_no_preview,
"worker_running": is_worker_running(),
"worker_last_processed": worker_last_processed().isoformat() if worker_last_processed() else None,
},
"taste_profile": {
"exists": taste is not None,
"track_count": taste.track_count if taste else 0,
"updated_at": taste.updated_at.isoformat() if taste else None,
},
"playlists": {
"total_generated": total_playlists,
"last_generated": last_playlist.isoformat() if last_playlist else None,
},
"skip_detector": {
"running": is_skip_detector_running(),
"active_sessions": len(get_skip_sessions()),
"sessions": [
{
"speaker_entity": entity,
"playlist_id": s.playlist_id,
"current_position": s.current_position,
"total_tracks": len(s.tracks),
"current_track": (
f"{s.tracks[s.current_position]['artist']} - {s.tracks[s.current_position]['title']}"
if s.current_position < len(s.tracks)
else None
),
"last_activity": s.last_activity_at.isoformat(),
}
for entity, s in get_skip_sessions().items()
],
},
},
"dependencies": {
"lastfm_api": "configured" if settings.lastfm_api_key else "not_configured",
"itunes_api": "ok", # no auth needed
"ha_reachable": bool(settings.ha_token),
"music_assistant_reachable": bool(settings.ha_token),
},
}