Files
AudiOhm/backend/app/services/library_service.py
T
root 801e6a050b prod: UI Optimisée mise en production
- Documentation archivée et réorganisée
- Backend: Ajout tests, migrations, library service, rate limiting
- Frontend: Suppression Flutter, focus sur interface web HTML/JS
- Tailwind CSS ajouté pour le style
- Améliorations UX et corrections bugs

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-20 09:56:39 +00:00

437 lines
12 KiB
Python

"""Library service."""
from datetime import datetime, timedelta, timezone
from typing import List, Optional
from uuid import UUID
from sqlalchemy import select, delete, update, func, and_, desc
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.listening_history import ListeningHistory
from app.models.liked_track import LikedTrack
from app.models.track import Track
class LibraryService:
"""Service for library operations (listening history and liked tracks)."""
def __init__(self, db: AsyncSession):
self.db = db
# ============ LISTENING HISTORY METHODS ============
async def add_to_listening_history(
self,
user_id: UUID,
track_id: UUID,
played_for: int,
completed: bool = False,
source: Optional[str] = None,
) -> ListeningHistory:
"""
Add a track to user's listening history.
Args:
user_id: User UUID
track_id: Track UUID
played_for: Duration played in seconds
completed: Whether track was played to completion
source: Playback source (library, playlist, search, etc.)
Returns:
Created listening history entry
"""
history_entry = ListeningHistory(
user_id=user_id,
track_id=track_id,
played_for=played_for,
completed=completed,
source=source,
played_at=datetime.now(timezone.utc).replace(tzinfo=None),
)
self.db.add(history_entry)
# Update track play count atomically
update_stmt = (
update(Track)
.where(Track.id == track_id)
.values(play_count=Track.play_count + 1)
)
await self.db.execute(update_stmt)
await self.db.commit()
await self.db.refresh(history_entry)
return history_entry
async def get_listening_history(
self,
user_id: UUID,
limit: int = 50,
offset: int = 0,
days: Optional[int] = None,
) -> List[ListeningHistory]:
"""
Get user's listening history.
Args:
user_id: User UUID
limit: Maximum results
offset: Pagination offset
days: Filter by last N days (None for all time)
Returns:
List of listening history entries
"""
stmt = (
select(ListeningHistory)
.where(ListeningHistory.user_id == user_id)
.options(selectinload(ListeningHistory.track))
.order_by(desc(ListeningHistory.played_at))
)
if days is not None:
cutoff_date = (datetime.now(timezone.utc) - timedelta(days=days)).replace(tzinfo=None)
stmt = stmt.where(ListeningHistory.played_at >= cutoff_date)
stmt = stmt.limit(limit).offset(offset)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def get_recently_played(
self,
user_id: UUID,
limit: int = 20,
) -> List[Track]:
"""
Get user's recently played tracks (unique tracks).
Args:
user_id: User UUID
limit: Maximum results
Returns:
List of unique recently played tracks
"""
# Subquery to get most recent play for each track
subquery = (
select(
ListeningHistory.track_id,
func.max(ListeningHistory.played_at).label("last_played"),
)
.where(ListeningHistory.user_id == user_id)
.group_by(ListeningHistory.track_id)
.order_by(desc("last_played"))
.limit(limit)
.subquery()
)
# Main query to get track details
stmt = (
select(Track)
.join(subquery, Track.id == subquery.c.track_id)
.order_by(desc(subquery.c.last_played))
)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def get_most_played_tracks(
self,
user_id: UUID,
limit: int = 20,
days: Optional[int] = None,
) -> List[tuple[Track, int]]:
"""
Get user's most played tracks.
Args:
user_id: User UUID
limit: Maximum results
days: Filter by last N days (None for all time)
Returns:
List of tuples (track, play_count)
"""
stmt = (
select(
Track,
func.count(ListeningHistory.id).label("play_count"),
)
.join(ListeningHistory, Track.id == ListeningHistory.track_id)
.where(ListeningHistory.user_id == user_id)
.group_by(Track.id)
.order_by(desc("play_count"))
.limit(limit)
)
if days is not None:
cutoff_date = (datetime.now(timezone.utc) - timedelta(days=days)).replace(tzinfo=None)
stmt = stmt.where(ListeningHistory.played_at >= cutoff_date)
result = await self.db.execute(stmt)
return [(row[0], row[1]) for row in result.all()]
async def clear_listening_history(
self,
user_id: UUID,
before_date: Optional[datetime] = None,
) -> int:
"""
Clear user's listening history.
Args:
user_id: User UUID
before_date: Clear history before this date (None for all)
Returns:
Number of entries deleted
"""
stmt = delete(ListeningHistory).where(ListeningHistory.user_id == user_id)
if before_date is not None:
stmt = stmt.where(ListeningHistory.played_at < before_date)
result = await self.db.execute(stmt)
await self.db.commit()
return result.rowcount
# ============ LIKED TRACKS METHODS ============
async def like_track(
self,
user_id: UUID,
track_id: UUID,
notes: Optional[str] = None,
) -> LikedTrack:
"""
Add a track to user's liked tracks.
Args:
user_id: User UUID
track_id: Track UUID
notes: Optional user notes
Returns:
Created liked track entry
Raises:
ValueError: If track is already liked
"""
# Check if already liked
existing_stmt = select(LikedTrack).where(
and_(
LikedTrack.user_id == user_id,
LikedTrack.track_id == track_id,
)
)
existing_result = await self.db.execute(existing_stmt)
existing = existing_result.scalar_one_or_none()
if existing:
raise ValueError("Track is already in liked tracks")
liked_track = LikedTrack(
user_id=user_id,
track_id=track_id,
notes=notes,
)
self.db.add(liked_track)
await self.db.commit()
await self.db.refresh(liked_track)
return liked_track
async def unlike_track(
self,
user_id: UUID,
track_id: UUID,
) -> None:
"""
Remove a track from user's liked tracks.
Args:
user_id: User UUID
track_id: Track UUID
Raises:
ValueError: If track is not in liked tracks
"""
stmt = select(LikedTrack).where(
and_(
LikedTrack.user_id == user_id,
LikedTrack.track_id == track_id,
)
)
result = await self.db.execute(stmt)
liked_track = result.scalar_one_or_none()
if not liked_track:
raise ValueError("Track is not in liked tracks")
await self.db.delete(liked_track)
await self.db.commit()
async def get_liked_tracks(
self,
user_id: UUID,
limit: int = 50,
offset: int = 0,
) -> List[LikedTrack]:
"""
Get user's liked tracks.
Args:
user_id: User UUID
limit: Maximum results
offset: Pagination offset
Returns:
List of liked track entries
"""
stmt = (
select(LikedTrack)
.where(LikedTrack.user_id == user_id)
.options(selectinload(LikedTrack.track))
.order_by(desc(LikedTrack.created_at))
.limit(limit)
.offset(offset)
)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def check_track_liked(
self,
user_id: UUID,
track_id: UUID,
) -> bool:
"""
Check if a track is in user's liked tracks.
Args:
user_id: User UUID
track_id: Track UUID
Returns:
True if track is liked, False otherwise
"""
stmt = select(LikedTrack).where(
and_(
LikedTrack.user_id == user_id,
LikedTrack.track_id == track_id,
)
)
result = await self.db.execute(stmt)
liked_track = result.scalar_one_or_none()
return liked_track is not None
async def update_liked_track_notes(
self,
user_id: UUID,
track_id: UUID,
notes: str,
) -> LikedTrack:
"""
Update notes for a liked track.
Args:
user_id: User UUID
track_id: Track UUID
notes: New notes
Returns:
Updated liked track entry
Raises:
ValueError: If track is not in liked tracks
"""
stmt = select(LikedTrack).where(
and_(
LikedTrack.user_id == user_id,
LikedTrack.track_id == track_id,
)
)
result = await self.db.execute(stmt)
liked_track = result.scalar_one_or_none()
if not liked_track:
raise ValueError("Track is not in liked tracks")
liked_track.notes = notes
liked_track.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
await self.db.commit()
await self.db.refresh(liked_track)
return liked_track
# ============ LIBRARY STATISTICS METHODS ============
async def get_library_stats(
self,
user_id: UUID,
) -> dict:
"""
Get user's library statistics.
Args:
user_id: User UUID
Returns:
Dictionary with library statistics
"""
# Total liked tracks
liked_count_stmt = (
select(func.count())
.select_from(LikedTrack)
.where(LikedTrack.user_id == user_id)
)
liked_count_result = await self.db.execute(liked_count_stmt)
liked_count = liked_count_result.scalar()
# Total plays
total_plays_stmt = (
select(func.count())
.select_from(ListeningHistory)
.where(ListeningHistory.user_id == user_id)
)
total_plays_result = await self.db.execute(total_plays_stmt)
total_plays = total_plays_result.scalar()
# Plays in last 30 days
thirty_days_ago = (datetime.now(timezone.utc) - timedelta(days=30)).replace(tzinfo=None)
recent_plays_stmt = (
select(func.count())
.select_from(ListeningHistory)
.where(
and_(
ListeningHistory.user_id == user_id,
ListeningHistory.played_at >= thirty_days_ago,
)
)
)
recent_plays_result = await self.db.execute(recent_plays_stmt)
recent_plays = recent_plays_result.scalar()
# Unique tracks played
unique_tracks_stmt = (
select(func.count(func.distinct(ListeningHistory.track_id)))
.select_from(ListeningHistory)
.where(ListeningHistory.user_id == user_id)
)
unique_tracks_result = await self.db.execute(unique_tracks_stmt)
unique_tracks = unique_tracks_result.scalar()
return {
"liked_tracks_count": liked_count,
"total_plays": total_plays,
"plays_last_30_days": recent_plays,
"unique_tracks_played": unique_tracks,
}