diff --git a/app/models/sound_played.py b/app/models/sound_played.py index 8a0e02a..c4a471d 100644 --- a/app/models/sound_played.py +++ b/app/models/sound_played.py @@ -77,7 +77,7 @@ class SoundPlayed(db.Model): @classmethod def create_play_record( cls, - user_id: int, + user_id: int | None, sound_id: int, *, commit: bool = True, diff --git a/app/services/music_player_service.py b/app/services/music_player_service.py index 313b14d..3c35a5c 100644 --- a/app/services/music_player_service.py +++ b/app/services/music_player_service.py @@ -3,18 +3,27 @@ import os import threading import time +from datetime import datetime from typing import Any, Optional +from zoneinfo import ZoneInfo import vlc from flask import current_app, request from app.models.playlist import Playlist from app.models.sound import Sound +from app.models.sound_played import SoundPlayed from app.services.logging_service import LoggingService from app.services.socketio_service import socketio_service logger = LoggingService.get_logger(__name__) +# Constants +TRACK_START_THRESHOLD_MS = 500 # 500 milliseconds - threshold for considering a track as "starting fresh" +STATE_CHANGE_THRESHOLD_MS = ( + 1000 # 1 second threshold for state change detection +) + class MusicPlayerService: """Service for managing a VLC music player with playlist support.""" @@ -47,6 +56,9 @@ class MusicPlayerService: self._track_ending_handled = ( False # Flag to prevent duplicate ending triggers ) + self._track_play_tracked = ( + False # Flag to track if current track play has been logged + ) def start_vlc_instance(self) -> bool: """Start a VLC instance with Python bindings.""" @@ -216,12 +228,41 @@ class MusicPlayerService: self.current_track_index = index # Reset track ending flag when loading a new track self._track_ending_handled = False + self._track_play_tracked = ( + False # Reset play tracking for new track + ) return True return False except Exception as e: logger.error(f"Error loading track at index {index}: {e}") return False + def _track_sound_play(self, sound_id: int) -> None: + """Track that a sound has been played.""" + try: + # Use stored app instance or current_app + app_to_use = self.app or current_app + if app_to_use: + with app_to_use.app_context(): + # Get the sound and increment its play count + sound = Sound.query.get(sound_id) + if sound: + sound.play_count += 1 + sound.updated_at = datetime.now(tz=ZoneInfo("UTC")) + logger.info( + f"Incremented play count for sound '{sound.name}' (ID: {sound_id})" + ) + + # Create a sound played record without user_id (anonymous play) + SoundPlayed.create_play_record( + user_id=None, sound_id=sound_id, commit=True + ) + logger.info( + f"Created anonymous play record for sound ID: {sound_id}" + ) + except Exception as e: + logger.error(f"Error tracking sound play for sound {sound_id}: {e}") + def _get_sound_file_path(self, sound: Sound) -> Optional[str]: """Get the file path for a sound, preferring normalized version.""" try: @@ -268,6 +309,9 @@ class MusicPlayerService: result = self.player.play() if result == 0: # Success self.is_playing = True + self._track_play_tracked = ( + False # Track when we first start playing + ) self._emit_player_state() return True return False @@ -430,9 +474,10 @@ class MusicPlayerService: if not self.current_playlist_id: return None - # Ensure we have Flask app context - if current_app: - with current_app.app_context(): + # Use stored app instance or current_app + app_to_use = self.app or current_app + if app_to_use: + with app_to_use.app_context(): playlist = Playlist.query.get(self.current_playlist_id) if playlist and 0 <= self.current_track_index < len( playlist.playlist_sounds @@ -610,11 +655,24 @@ class MusicPlayerService: elif self.is_playing and not old_playing: self._track_ending_handled = False + # Track play event when song starts playing from beginning (but only once per track load) + # Only track if we're playing, haven't tracked yet, and current time is near the start (< 5 seconds) + if ( + self.is_playing + and not self._track_play_tracked + and self.current_time >= 0 + and self.current_time < TRACK_START_THRESHOLD_MS + ): + current_track = self.get_current_track() + if current_track: + self._track_sound_play(current_track["id"]) + self._track_play_tracked = True + # Emit updates if state changed significantly or periodically state_changed = ( old_playing != self.is_playing or abs(old_time - self.current_time) - > 1000 # More than 1 second difference + > STATE_CHANGE_THRESHOLD_MS # More than 1 second difference ) # Always emit if playing to keep frontend updated