Compare commits

..

2 Commits

Author SHA1 Message Date
JSC
95e166eefb feat: Add endpoint and service method to retrieve top users by various metrics
Some checks failed
Backend CI / lint (push) Failing after 9s
Backend CI / test (push) Failing after 1m36s
2025-09-27 21:52:00 +02:00
JSC
d9697c2dd7 feat: Add TTS statistics endpoint and service method for comprehensive TTS data 2025-09-27 21:37:59 +02:00
5 changed files with 221 additions and 5 deletions

View File

@@ -41,6 +41,56 @@ async def get_track_statistics(
) from e ) from e
@router.get("/tts-statistics")
async def get_tts_statistics(
_current_user: Annotated[User, Depends(get_current_user)],
dashboard_service: Annotated[DashboardService, Depends(get_dashboard_service)],
) -> dict[str, Any]:
"""Get TTS statistics."""
try:
return await dashboard_service.get_tts_statistics()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch TTS statistics: {e!s}",
) from e
@router.get("/top-users")
async def get_top_users(
_current_user: Annotated[User, Depends(get_current_user)],
dashboard_service: Annotated[DashboardService, Depends(get_dashboard_service)],
metric_type: Annotated[
str,
Query(
description="Metric type: sounds_played, credits_used, tracks_added, tts_added, playlists_created",
),
],
period: Annotated[
str,
Query(
description="Time period (today, 1_day, 1_week, 1_month, 1_year, all_time)",
),
] = "all_time",
limit: Annotated[
int,
Query(description="Number of top users to return", ge=1, le=100),
] = 10,
) -> list[dict[str, Any]]:
"""Get top users by metric for a specific period."""
try:
return await dashboard_service.get_top_users(
metric_type=metric_type,
period=period,
limit=limit,
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch top users: {e!s}",
) from e
@router.get("/top-sounds") @router.get("/top-sounds")
async def get_top_sounds( async def get_top_sounds(
_current_user: Annotated[User, Depends(get_current_user)], _current_user: Annotated[User, Depends(get_current_user)],

View File

@@ -9,6 +9,7 @@ from app.core.database import get_db
from app.core.logging import get_logger from app.core.logging import get_logger
from app.models.user import User from app.models.user import User
from app.repositories.sound import SoundRepository from app.repositories.sound import SoundRepository
from app.repositories.user import UserRepository
from app.services.auth import AuthService from app.services.auth import AuthService
from app.services.dashboard import DashboardService from app.services.dashboard import DashboardService
from app.services.oauth import OAuthService from app.services.oauth import OAuthService
@@ -193,4 +194,5 @@ async def get_dashboard_service(
) -> DashboardService: ) -> DashboardService:
"""Get the dashboard service.""" """Get the dashboard service."""
sound_repository = SoundRepository(session) sound_repository = SoundRepository(session)
return DashboardService(sound_repository) user_repository = UserRepository(session)
return DashboardService(sound_repository, user_repository)

View File

@@ -201,8 +201,8 @@ class SoundRepository(BaseRepository[Sound]):
) )
raise raise
async def get_soundboard_statistics(self) -> dict[str, int | float]: async def get_soundboard_statistics(self, sound_type: str = "SDB") -> dict[str, int | float]:
"""Get statistics for SDB type sounds.""" """Get statistics for sounds of a specific type."""
try: try:
statement = select( statement = select(
func.count(Sound.id).label("count"), func.count(Sound.id).label("count"),
@@ -211,7 +211,7 @@ class SoundRepository(BaseRepository[Sound]):
func.sum( func.sum(
Sound.size + func.coalesce(Sound.normalized_size, 0), Sound.size + func.coalesce(Sound.normalized_size, 0),
).label("total_size"), ).label("total_size"),
).where(Sound.type == "SDB") ).where(Sound.type == sound_type)
result = await self.session.exec(statement) result = await self.session.exec(statement)
row = result.first() row = result.first()

View File

@@ -1,5 +1,6 @@
"""User repository.""" """User repository."""
from datetime import datetime
from enum import Enum from enum import Enum
from typing import Any from typing import Any
@@ -11,6 +12,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.logging import get_logger from app.core.logging import get_logger
from app.models.plan import Plan from app.models.plan import Plan
from app.models.user import User from app.models.user import User
from app.models.sound_played import SoundPlayed
from app.models.credit_transaction import CreditTransaction
from app.models.playlist import Playlist
from app.models.sound import Sound
from app.models.tts import TTS
from app.repositories.base import BaseRepository from app.repositories.base import BaseRepository
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -217,3 +223,110 @@ class UserRepository(BaseRepository[User]):
except Exception: except Exception:
logger.exception("Failed to check if email exists: %s", email) logger.exception("Failed to check if email exists: %s", email)
raise raise
async def get_top_users(
self,
metric_type: str,
date_filter: datetime | None = None,
limit: int = 10,
) -> list[dict[str, Any]]:
"""Get top users by different metrics."""
try:
if metric_type == "sounds_played":
# Get users with most sounds played
query = (
select(
User.id,
User.name,
func.count(SoundPlayed.id).label("count")
)
.join(SoundPlayed, User.id == SoundPlayed.user_id)
.group_by(User.id, User.name)
)
if date_filter:
query = query.where(SoundPlayed.created_at >= date_filter)
elif metric_type == "credits_used":
# Get users with most credits used (negative transactions)
query = (
select(
User.id,
User.name,
func.sum(func.abs(CreditTransaction.amount)).label("count")
)
.join(CreditTransaction, User.id == CreditTransaction.user_id)
.where(CreditTransaction.amount < 0)
.group_by(User.id, User.name)
)
if date_filter:
query = query.where(CreditTransaction.created_at >= date_filter)
elif metric_type == "tracks_added":
# Get users with most EXT sounds added
query = (
select(
User.id,
User.name,
func.count(Sound.id).label("count")
)
.join(Sound, User.id == Sound.user_id)
.where(Sound.type == "EXT")
.group_by(User.id, User.name)
)
if date_filter:
query = query.where(Sound.created_at >= date_filter)
elif metric_type == "tts_added":
# Get users with most TTS sounds added
query = (
select(
User.id,
User.name,
func.count(TTS.id).label("count")
)
.join(TTS, User.id == TTS.user_id)
.group_by(User.id, User.name)
)
if date_filter:
query = query.where(TTS.created_at >= date_filter)
elif metric_type == "playlists_created":
# Get users with most playlists created
query = (
select(
User.id,
User.name,
func.count(Playlist.id).label("count")
)
.join(Playlist, User.id == Playlist.user_id)
.group_by(User.id, User.name)
)
if date_filter:
query = query.where(Playlist.created_at >= date_filter)
else:
msg = f"Unknown metric type: {metric_type}"
raise ValueError(msg)
# Add ordering and limit
query = query.order_by(func.count().desc()).limit(limit)
result = await self.session.exec(query)
rows = result.all()
return [
{
"id": row[0],
"name": row[1],
"count": int(row[2]),
}
for row in rows
]
except Exception:
logger.exception(
"Failed to get top users for metric=%s, date_filter=%s",
metric_type,
date_filter,
)
raise

View File

@@ -5,6 +5,7 @@ from typing import Any
from app.core.logging import get_logger from app.core.logging import get_logger
from app.repositories.sound import SoundRepository from app.repositories.sound import SoundRepository
from app.repositories.user import UserRepository
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -12,9 +13,10 @@ logger = get_logger(__name__)
class DashboardService: class DashboardService:
"""Service for dashboard statistics and analytics.""" """Service for dashboard statistics and analytics."""
def __init__(self, sound_repository: SoundRepository) -> None: def __init__(self, sound_repository: SoundRepository, user_repository: UserRepository) -> None:
"""Initialize the dashboard service.""" """Initialize the dashboard service."""
self.sound_repository = sound_repository self.sound_repository = sound_repository
self.user_repository = user_repository
async def get_soundboard_statistics(self) -> dict[str, Any]: async def get_soundboard_statistics(self) -> dict[str, Any]:
"""Get comprehensive soundboard statistics.""" """Get comprehensive soundboard statistics."""
@@ -85,6 +87,55 @@ class DashboardService:
) )
raise raise
async def get_tts_statistics(self) -> dict[str, Any]:
"""Get comprehensive TTS statistics."""
try:
stats = await self.sound_repository.get_soundboard_statistics("TTS")
return {
"sound_count": stats["count"],
"total_play_count": stats["total_plays"],
"total_duration": stats["total_duration"],
"total_size": stats["total_size"],
}
except Exception:
logger.exception("Failed to get TTS statistics")
raise
async def get_top_users(
self,
metric_type: str,
period: str = "all_time",
limit: int = 10,
) -> list[dict[str, Any]]:
"""Get top users by different metrics for a specific period."""
try:
# Calculate the date filter based on period
date_filter = self._get_date_filter(period)
# Get top users from repository
top_users = await self.user_repository.get_top_users(
metric_type=metric_type,
date_filter=date_filter,
limit=limit,
)
return [
{
"id": user["id"],
"name": user["name"],
"count": user["count"],
}
for user in top_users
]
except Exception:
logger.exception(
"Failed to get top users for metric=%s, period=%s",
metric_type,
period,
)
raise
def _get_date_filter(self, period: str) -> datetime | None: # noqa: PLR0911 def _get_date_filter(self, period: str) -> datetime | None: # noqa: PLR0911
"""Calculate the date filter based on the period.""" """Calculate the date filter based on the period."""
now = datetime.now(UTC) now = datetime.now(UTC)