Compare commits
2 Commits
4f702d3302
...
842e1dff13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842e1dff13 | ||
|
|
93897921fb |
@@ -100,6 +100,7 @@ def create_app():
|
|||||||
auth,
|
auth,
|
||||||
main,
|
main,
|
||||||
player,
|
player,
|
||||||
|
playlist,
|
||||||
soundboard,
|
soundboard,
|
||||||
sounds,
|
sounds,
|
||||||
stream,
|
stream,
|
||||||
@@ -113,6 +114,7 @@ def create_app():
|
|||||||
app.register_blueprint(sounds.bp, url_prefix="/api/sounds")
|
app.register_blueprint(sounds.bp, url_prefix="/api/sounds")
|
||||||
app.register_blueprint(stream.bp, url_prefix="/api/stream")
|
app.register_blueprint(stream.bp, url_prefix="/api/stream")
|
||||||
app.register_blueprint(player.bp, url_prefix="/api/player")
|
app.register_blueprint(player.bp, url_prefix="/api/player")
|
||||||
|
app.register_blueprint(playlist.bp, url_prefix="/api/playlists")
|
||||||
|
|
||||||
# Shutdown services when app is torn down
|
# Shutdown services when app is torn down
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
|
|||||||
@@ -276,3 +276,14 @@ class Playlist(db.Model):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return new_playlist
|
return new_playlist
|
||||||
|
|
||||||
|
def save(self, commit: bool = True) -> None:
|
||||||
|
"""Save changes to the playlist."""
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def delete(self, commit: bool = True) -> None:
|
||||||
|
"""Delete the playlist."""
|
||||||
|
db.session.delete(self)
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
|||||||
250
app/routes/playlist.py
Normal file
250
app/routes/playlist.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
"""Playlist management routes."""
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from app.models.playlist import Playlist
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.services.decorators import require_auth
|
||||||
|
from app.services.music_player_service import music_player_service
|
||||||
|
from app.services.logging_service import LoggingService
|
||||||
|
|
||||||
|
logger = LoggingService.get_logger(__name__)
|
||||||
|
|
||||||
|
bp = Blueprint("playlist", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/", methods=["GET"])
|
||||||
|
@require_auth
|
||||||
|
def get_playlists():
|
||||||
|
"""Get all playlists."""
|
||||||
|
try:
|
||||||
|
# Get system playlists and user playlists
|
||||||
|
system_playlists = Playlist.find_system_playlists()
|
||||||
|
user_playlists = [] # TODO: Add user-specific playlists when user auth is implemented
|
||||||
|
|
||||||
|
playlists = system_playlists + user_playlists
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"playlists": [playlist.to_dict() for playlist in playlists]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting playlists: {e}")
|
||||||
|
return jsonify({"error": "Failed to get playlists"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>", methods=["GET"])
|
||||||
|
@require_auth
|
||||||
|
def get_playlist(playlist_id):
|
||||||
|
"""Get a specific playlist with sounds."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
return jsonify({"playlist": playlist.to_detailed_dict()})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to get playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/", methods=["POST"])
|
||||||
|
@require_auth
|
||||||
|
def create_playlist():
|
||||||
|
"""Create a new playlist."""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
name = data.get("name")
|
||||||
|
description = data.get("description")
|
||||||
|
genre = data.get("genre")
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({"error": "Playlist name is required"}), 400
|
||||||
|
|
||||||
|
# Check if playlist with same name already exists
|
||||||
|
existing = Playlist.find_by_name(name)
|
||||||
|
if existing:
|
||||||
|
return jsonify({"error": "Playlist with this name already exists"}), 400
|
||||||
|
|
||||||
|
playlist = Playlist.create_playlist(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
genre=genre,
|
||||||
|
user_id=None, # System playlist for now
|
||||||
|
is_deletable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({"playlist": playlist.to_dict()}), 201
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating playlist: {e}")
|
||||||
|
return jsonify({"error": "Failed to create playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>", methods=["PUT"])
|
||||||
|
@require_auth
|
||||||
|
def update_playlist(playlist_id):
|
||||||
|
"""Update a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if "name" in data:
|
||||||
|
playlist.name = data["name"]
|
||||||
|
if "description" in data:
|
||||||
|
playlist.description = data["description"]
|
||||||
|
if "genre" in data:
|
||||||
|
playlist.genre = data["genre"]
|
||||||
|
|
||||||
|
playlist.save()
|
||||||
|
|
||||||
|
return jsonify({"playlist": playlist.to_dict()})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to update playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>", methods=["DELETE"])
|
||||||
|
@require_auth
|
||||||
|
def delete_playlist(playlist_id):
|
||||||
|
"""Delete a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
|
||||||
|
if not playlist.is_deletable:
|
||||||
|
return jsonify({"error": "This playlist cannot be deleted"}), 400
|
||||||
|
|
||||||
|
# If this is the current playlist, clear it from the player
|
||||||
|
current_playlist = Playlist.find_current_playlist()
|
||||||
|
if current_playlist and current_playlist.id == playlist_id:
|
||||||
|
# Set main playlist as current if it exists
|
||||||
|
main_playlist = Playlist.find_main_playlist()
|
||||||
|
if main_playlist:
|
||||||
|
main_playlist.set_as_current()
|
||||||
|
music_player_service.reload_current_playlist_if_modified(main_playlist.id)
|
||||||
|
|
||||||
|
playlist.delete()
|
||||||
|
|
||||||
|
return jsonify({"message": "Playlist deleted successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to delete playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>/set-current", methods=["POST"])
|
||||||
|
@require_auth
|
||||||
|
def set_current_playlist(playlist_id):
|
||||||
|
"""Set a playlist as the current one."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
playlist.set_as_current()
|
||||||
|
|
||||||
|
# Reload the playlist in the music player
|
||||||
|
music_player_service.reload_current_playlist_if_modified(playlist_id)
|
||||||
|
|
||||||
|
return jsonify({"message": "Playlist set as current"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting current playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to set current playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>/sounds", methods=["POST"])
|
||||||
|
@require_auth
|
||||||
|
def add_sound_to_playlist(playlist_id):
|
||||||
|
"""Add a sound to a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
data = request.get_json()
|
||||||
|
sound_id = data.get("sound_id")
|
||||||
|
order = data.get("order")
|
||||||
|
|
||||||
|
if not sound_id:
|
||||||
|
return jsonify({"error": "Sound ID is required"}), 400
|
||||||
|
|
||||||
|
# Verify sound exists
|
||||||
|
sound = Sound.query.get_or_404(sound_id)
|
||||||
|
|
||||||
|
# Add sound to playlist
|
||||||
|
playlist_sound = playlist.add_sound(sound_id, order)
|
||||||
|
|
||||||
|
# Reload playlist in music player if it's the current one
|
||||||
|
music_player_service.reload_current_playlist_if_modified(playlist_id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"message": "Sound added to playlist",
|
||||||
|
"playlist_sound": {
|
||||||
|
"sound_id": playlist_sound.sound_id,
|
||||||
|
"order": playlist_sound.order
|
||||||
|
}
|
||||||
|
}), 201
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding sound to playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to add sound to playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>/sounds/<int:sound_id>", methods=["DELETE"])
|
||||||
|
@require_auth
|
||||||
|
def remove_sound_from_playlist(playlist_id, sound_id):
|
||||||
|
"""Remove a sound from a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
|
||||||
|
success = playlist.remove_sound(sound_id)
|
||||||
|
if not success:
|
||||||
|
return jsonify({"error": "Sound not found in playlist"}), 404
|
||||||
|
|
||||||
|
# Reload playlist in music player if it's the current one
|
||||||
|
music_player_service.reload_current_playlist_if_modified(playlist_id)
|
||||||
|
|
||||||
|
return jsonify({"message": "Sound removed from playlist"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error removing sound from playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to remove sound from playlist"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>/sounds/reorder", methods=["PUT"])
|
||||||
|
@require_auth
|
||||||
|
def reorder_playlist_sounds(playlist_id):
|
||||||
|
"""Reorder sounds in a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
data = request.get_json()
|
||||||
|
sound_orders = data.get("sound_orders", [])
|
||||||
|
|
||||||
|
if not sound_orders:
|
||||||
|
return jsonify({"error": "Sound orders are required"}), 400
|
||||||
|
|
||||||
|
# Validate sound_orders format
|
||||||
|
for item in sound_orders:
|
||||||
|
if not isinstance(item, dict) or "sound_id" not in item or "order" not in item:
|
||||||
|
return jsonify({"error": "Invalid sound_orders format"}), 400
|
||||||
|
|
||||||
|
# Reorder sounds
|
||||||
|
playlist.reorder_sounds(sound_orders)
|
||||||
|
|
||||||
|
# Reload playlist in music player if it's the current one
|
||||||
|
music_player_service.reload_current_playlist_if_modified(playlist_id)
|
||||||
|
|
||||||
|
return jsonify({"message": "Playlist sounds reordered"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reordering playlist sounds {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to reorder playlist sounds"}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/<int:playlist_id>/duplicate", methods=["POST"])
|
||||||
|
@require_auth
|
||||||
|
def duplicate_playlist(playlist_id):
|
||||||
|
"""Duplicate a playlist."""
|
||||||
|
try:
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
data = request.get_json()
|
||||||
|
new_name = data.get("name")
|
||||||
|
|
||||||
|
if not new_name:
|
||||||
|
return jsonify({"error": "New playlist name is required"}), 400
|
||||||
|
|
||||||
|
# Check if playlist with same name already exists
|
||||||
|
existing = Playlist.find_by_name(new_name)
|
||||||
|
if existing:
|
||||||
|
return jsonify({"error": "Playlist with this name already exists"}), 400
|
||||||
|
|
||||||
|
new_playlist = playlist.duplicate(new_name)
|
||||||
|
|
||||||
|
return jsonify({"playlist": new_playlist.to_dict()}), 201
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error duplicating playlist {playlist_id}: {e}")
|
||||||
|
return jsonify({"error": "Failed to duplicate playlist"}), 500
|
||||||
@@ -38,11 +38,15 @@ class MusicPlayerService:
|
|||||||
self.current_time = 0
|
self.current_time = 0
|
||||||
self.duration = 0
|
self.duration = 0
|
||||||
self.last_sync_time = 0
|
self.last_sync_time = 0
|
||||||
self.sync_interval = 0.5 # seconds (increased frequency to catch track endings)
|
self.sync_interval = (
|
||||||
|
0.5 # seconds (increased frequency to catch track endings)
|
||||||
|
)
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self._sync_thread = None
|
self._sync_thread = None
|
||||||
self._stop_sync = False
|
self._stop_sync = False
|
||||||
self._track_ending_handled = False # Flag to prevent duplicate ending triggers
|
self._track_ending_handled = (
|
||||||
|
False # Flag to prevent duplicate ending triggers
|
||||||
|
)
|
||||||
|
|
||||||
def start_vlc_instance(self) -> bool:
|
def start_vlc_instance(self) -> bool:
|
||||||
"""Start a VLC instance with Python bindings."""
|
"""Start a VLC instance with Python bindings."""
|
||||||
@@ -69,8 +73,8 @@ class MusicPlayerService:
|
|||||||
|
|
||||||
logger.info("VLC music player started successfully")
|
logger.info("VLC music player started successfully")
|
||||||
|
|
||||||
# Automatically load the main playlist
|
# Automatically load the current playlist
|
||||||
self._load_main_playlist_on_startup()
|
self._load_current_playlist_on_startup()
|
||||||
|
|
||||||
self._start_sync_thread()
|
self._start_sync_thread()
|
||||||
return True
|
return True
|
||||||
@@ -100,7 +104,7 @@ class MusicPlayerService:
|
|||||||
logger.error(f"Error stopping VLC instance: {e}")
|
logger.error(f"Error stopping VLC instance: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def load_playlist(self, playlist_id: int) -> bool:
|
def load_playlist(self, playlist_id: int, reload: bool = False) -> bool:
|
||||||
"""Load a playlist into VLC."""
|
"""Load a playlist into VLC."""
|
||||||
try:
|
try:
|
||||||
if not self.instance or not self.player:
|
if not self.instance or not self.player:
|
||||||
@@ -115,7 +119,9 @@ class MusicPlayerService:
|
|||||||
if not playlist:
|
if not playlist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._load_playlist_with_context(playlist)
|
return self._load_playlist_with_context(
|
||||||
|
playlist, reload
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback for when no Flask context is available
|
# Fallback for when no Flask context is available
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -126,12 +132,14 @@ class MusicPlayerService:
|
|||||||
logger.error(f"Error loading playlist {playlist_id}: {e}")
|
logger.error(f"Error loading playlist {playlist_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _build_thumbnail_url(self, sound_type: str, thumbnail_filename: str) -> str:
|
def _build_thumbnail_url(
|
||||||
|
self, sound_type: str, thumbnail_filename: str
|
||||||
|
) -> str:
|
||||||
"""Build absolute thumbnail URL."""
|
"""Build absolute thumbnail URL."""
|
||||||
try:
|
try:
|
||||||
# Try to get base URL from current request context
|
# Try to get base URL from current request context
|
||||||
if request:
|
if request:
|
||||||
base_url = request.url_root.rstrip('/')
|
base_url = request.url_root.rstrip("/")
|
||||||
else:
|
else:
|
||||||
# Fallback to localhost if no request context
|
# Fallback to localhost if no request context
|
||||||
base_url = "http://localhost:5000"
|
base_url = "http://localhost:5000"
|
||||||
@@ -140,7 +148,9 @@ class MusicPlayerService:
|
|||||||
# Fallback if request context is not available
|
# Fallback if request context is not available
|
||||||
return f"http://localhost:5000/api/sounds/{sound_type.lower()}/thumbnails/{thumbnail_filename}"
|
return f"http://localhost:5000/api/sounds/{sound_type.lower()}/thumbnails/{thumbnail_filename}"
|
||||||
|
|
||||||
def _load_playlist_with_context(self, playlist) -> bool:
|
def _load_playlist_with_context(
|
||||||
|
self, playlist, reload: bool = False
|
||||||
|
) -> bool:
|
||||||
"""Load playlist with database context already established."""
|
"""Load playlist with database context already established."""
|
||||||
try:
|
try:
|
||||||
# Clear current playlist
|
# Clear current playlist
|
||||||
@@ -156,6 +166,26 @@ class MusicPlayerService:
|
|||||||
if file_path and os.path.exists(file_path):
|
if file_path and os.path.exists(file_path):
|
||||||
self.playlist_files.append(file_path)
|
self.playlist_files.append(file_path)
|
||||||
|
|
||||||
|
deleted = False
|
||||||
|
if reload:
|
||||||
|
# Set current track index to the real index of the current track
|
||||||
|
# in case the order has changed or the track has been deleted
|
||||||
|
current_track = self.get_current_track()
|
||||||
|
current_track_id = (
|
||||||
|
current_track["id"] if current_track else None
|
||||||
|
)
|
||||||
|
sound_ids = [
|
||||||
|
ps.sound.id
|
||||||
|
for ps in sorted(
|
||||||
|
playlist.playlist_sounds, key=lambda x: x.order
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if current_track_id in sound_ids:
|
||||||
|
self.current_track_index = sound_ids.index(current_track_id)
|
||||||
|
else:
|
||||||
|
deleted = True
|
||||||
|
|
||||||
|
if not reload or deleted:
|
||||||
self.current_playlist_id = playlist.id
|
self.current_playlist_id = playlist.id
|
||||||
self.current_track_index = 0
|
self.current_track_index = 0
|
||||||
|
|
||||||
@@ -274,7 +304,7 @@ class MusicPlayerService:
|
|||||||
logger.error(f"Error stopping playback: {e}")
|
logger.error(f"Error stopping playback: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def next_track(self) -> bool:
|
def next_track(self, force_play: bool = False) -> bool:
|
||||||
"""Skip to next track."""
|
"""Skip to next track."""
|
||||||
try:
|
try:
|
||||||
if not self.playlist_files:
|
if not self.playlist_files:
|
||||||
@@ -297,7 +327,7 @@ class MusicPlayerService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
if self._load_track_at_index(next_index):
|
if self._load_track_at_index(next_index):
|
||||||
if self.is_playing:
|
if self.is_playing or force_play:
|
||||||
self.play()
|
self.play()
|
||||||
self._emit_player_state()
|
self._emit_player_state()
|
||||||
return True
|
return True
|
||||||
@@ -422,7 +452,9 @@ class MusicPlayerService:
|
|||||||
"artist": None, # Could be extracted from metadata
|
"artist": None, # Could be extracted from metadata
|
||||||
"duration": sound.duration or 0,
|
"duration": sound.duration or 0,
|
||||||
"thumbnail": (
|
"thumbnail": (
|
||||||
self._build_thumbnail_url(sound.type, sound.thumbnail)
|
self._build_thumbnail_url(
|
||||||
|
sound.type, sound.thumbnail
|
||||||
|
)
|
||||||
if sound.thumbnail
|
if sound.thumbnail
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
@@ -457,7 +489,9 @@ class MusicPlayerService:
|
|||||||
"artist": None,
|
"artist": None,
|
||||||
"duration": sound.duration or 0,
|
"duration": sound.duration or 0,
|
||||||
"thumbnail": (
|
"thumbnail": (
|
||||||
self._build_thumbnail_url(sound.type, sound.thumbnail)
|
self._build_thumbnail_url(
|
||||||
|
sound.type, sound.thumbnail
|
||||||
|
)
|
||||||
if sound.thumbnail
|
if sound.thumbnail
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
@@ -471,13 +505,15 @@ class MusicPlayerService:
|
|||||||
|
|
||||||
def get_player_state(self) -> dict[str, Any]:
|
def get_player_state(self) -> dict[str, Any]:
|
||||||
"""Get complete player state."""
|
"""Get complete player state."""
|
||||||
|
current_track = self.get_current_track()
|
||||||
return {
|
return {
|
||||||
"is_playing": self.is_playing,
|
"is_playing": self.is_playing,
|
||||||
"current_time": self.current_time,
|
"current_time": self.current_time,
|
||||||
"duration": self.duration,
|
"duration": self.duration,
|
||||||
"volume": self.volume,
|
"volume": self.volume,
|
||||||
"play_mode": self.play_mode,
|
"play_mode": self.play_mode,
|
||||||
"current_track": self.get_current_track(),
|
"current_track": current_track,
|
||||||
|
"current_track_id": current_track["id"] if current_track else None,
|
||||||
"current_track_index": self.current_track_index,
|
"current_track_index": self.current_track_index,
|
||||||
"playlist": self.get_playlist_tracks(),
|
"playlist": self.get_playlist_tracks(),
|
||||||
"playlist_id": self.current_playlist_id,
|
"playlist_id": self.current_playlist_id,
|
||||||
@@ -533,14 +569,22 @@ class MusicPlayerService:
|
|||||||
# Check for ended state
|
# Check for ended state
|
||||||
if state == vlc.State.Ended:
|
if state == vlc.State.Ended:
|
||||||
track_ended = True
|
track_ended = True
|
||||||
logger.info(f"Track ended via VLC State.Ended, mode: {self.play_mode}")
|
logger.info(
|
||||||
|
f"Track ended via VLC State.Ended, mode: {self.play_mode}"
|
||||||
|
)
|
||||||
|
|
||||||
# Also check if we're very close to the end (within 500ms) and not playing
|
# Also check if we're very close to the end (within 500ms) and not playing
|
||||||
elif (self.duration > 0 and self.current_time > 0 and
|
elif (
|
||||||
self.current_time >= (self.duration - 500) and
|
self.duration > 0
|
||||||
not self.is_playing and old_playing):
|
and self.current_time > 0
|
||||||
|
and self.current_time >= (self.duration - 500)
|
||||||
|
and not self.is_playing
|
||||||
|
and old_playing
|
||||||
|
):
|
||||||
track_ended = True
|
track_ended = True
|
||||||
logger.info(f"Track ended via time check, mode: {self.play_mode}")
|
logger.info(
|
||||||
|
f"Track ended via time check, mode: {self.play_mode}"
|
||||||
|
)
|
||||||
|
|
||||||
# Handle track ending based on play mode (only if not already handled)
|
# Handle track ending based on play mode (only if not already handled)
|
||||||
if track_ended and not self._track_ending_handled:
|
if track_ended and not self._track_ending_handled:
|
||||||
@@ -548,20 +592,17 @@ class MusicPlayerService:
|
|||||||
|
|
||||||
if self.play_mode == "loop-one":
|
if self.play_mode == "loop-one":
|
||||||
logger.info("Restarting track for loop-one mode")
|
logger.info("Restarting track for loop-one mode")
|
||||||
# Stop first, then reload and play
|
self.play_track_at_index(self.current_track_index)
|
||||||
self.player.stop()
|
elif self.play_mode in [
|
||||||
# Reload the current track
|
"continuous",
|
||||||
if (self.current_track_index < len(self.playlist_files)):
|
"loop-playlist",
|
||||||
media = self.instance.media_new(
|
"random",
|
||||||
self.playlist_files[self.current_track_index]
|
]:
|
||||||
|
logger.info(
|
||||||
|
f"Advancing to next track for {self.play_mode} mode"
|
||||||
)
|
)
|
||||||
self.player.set_media(media)
|
self.next_track(True)
|
||||||
self.player.play()
|
|
||||||
# Reset the flag after a short delay to allow for new track
|
|
||||||
self._track_ending_handled = False
|
|
||||||
elif self.play_mode in ["continuous", "loop-playlist", "random"]:
|
|
||||||
logger.info(f"Advancing to next track for {self.play_mode} mode")
|
|
||||||
self.next_track()
|
|
||||||
# Reset the flag after track change
|
# Reset the flag after track change
|
||||||
self._track_ending_handled = False
|
self._track_ending_handled = False
|
||||||
|
|
||||||
@@ -596,7 +637,9 @@ class MusicPlayerService:
|
|||||||
with app_to_use.app_context():
|
with app_to_use.app_context():
|
||||||
state = self.get_player_state()
|
state = self.get_player_state()
|
||||||
socketio_service.emit_to_all("player_state_update", state)
|
socketio_service.emit_to_all("player_state_update", state)
|
||||||
logger.info(f"Emitted player state: playing={state['is_playing']}, time={state['current_time']}, track={state.get('current_track', {}).get('title', 'None')}")
|
logger.info(
|
||||||
|
f"Emitted player state: playing={state['is_playing']}, time={state['current_time']}, track={state.get('current_track', {}).get('title', 'None')}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback when no Flask context - emit basic state without database queries
|
# Fallback when no Flask context - emit basic state without database queries
|
||||||
basic_state = {
|
basic_state = {
|
||||||
@@ -606,12 +649,15 @@ class MusicPlayerService:
|
|||||||
"volume": self.volume,
|
"volume": self.volume,
|
||||||
"play_mode": self.play_mode,
|
"play_mode": self.play_mode,
|
||||||
"current_track": None,
|
"current_track": None,
|
||||||
|
"current_track_id": None,
|
||||||
"current_track_index": self.current_track_index,
|
"current_track_index": self.current_track_index,
|
||||||
"playlist": [],
|
"playlist": [],
|
||||||
"playlist_id": self.current_playlist_id,
|
"playlist_id": self.current_playlist_id,
|
||||||
}
|
}
|
||||||
socketio_service.emit_to_all("player_state_update", basic_state)
|
socketio_service.emit_to_all("player_state_update", basic_state)
|
||||||
logger.info(f"Emitted basic player state: playing={basic_state['is_playing']}, time={basic_state['current_time']}")
|
logger.info(
|
||||||
|
f"Emitted basic player state: playing={basic_state['is_playing']}, time={basic_state['current_time']}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error emitting player state: {e}")
|
logger.debug(f"Error emitting player state: {e}")
|
||||||
|
|
||||||
@@ -635,29 +681,74 @@ class MusicPlayerService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error syncing VLC state: {e}")
|
logger.debug(f"Error syncing VLC state: {e}")
|
||||||
|
|
||||||
|
def _load_current_playlist_on_startup(self):
|
||||||
def _load_main_playlist_on_startup(self):
|
"""Load the current playlist automatically on startup."""
|
||||||
"""Load the main playlist automatically on startup."""
|
|
||||||
try:
|
try:
|
||||||
if not self.app:
|
if not self.app:
|
||||||
logger.warning("No Flask app context available, skipping main playlist load")
|
logger.warning(
|
||||||
|
"No Flask app context available, skipping current playlist load"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
with self.app.app_context():
|
with self.app.app_context():
|
||||||
# Find the main playlist
|
# Find the current playlist
|
||||||
main_playlist = Playlist.find_main_playlist()
|
current_playlist = Playlist.find_current_playlist()
|
||||||
|
|
||||||
if main_playlist:
|
if current_playlist:
|
||||||
success = self.load_playlist(main_playlist.id)
|
success = self.load_playlist(current_playlist.id)
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"Automatically loaded main playlist '{main_playlist.name}' with {len(self.playlist_files)} tracks")
|
logger.info(
|
||||||
|
f"Automatically loaded current playlist '{current_playlist.name}' with {len(self.playlist_files)} tracks"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("Failed to load main playlist on startup")
|
logger.warning(
|
||||||
|
"Failed to load current playlist on startup"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("No main playlist found to load on startup")
|
logger.info("No current playlist found to load on startup")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading main playlist on startup: {e}")
|
logger.error(f"Error loading current playlist on startup: {e}")
|
||||||
|
|
||||||
|
def reload_current_playlist_if_modified(
|
||||||
|
self, modified_playlist_id: int
|
||||||
|
) -> bool:
|
||||||
|
"""Reload the current playlist if it's the one that was modified."""
|
||||||
|
try:
|
||||||
|
if not self.app:
|
||||||
|
logger.warning(
|
||||||
|
"No Flask app context available, skipping playlist reload"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
# Find the current playlist
|
||||||
|
current_playlist = Playlist.find_current_playlist()
|
||||||
|
|
||||||
|
if (
|
||||||
|
current_playlist
|
||||||
|
and current_playlist.id == modified_playlist_id
|
||||||
|
):
|
||||||
|
# Reload the playlist
|
||||||
|
success = self.load_playlist(current_playlist.id, True)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(
|
||||||
|
f"Reloaded current playlist '{current_playlist.name}' after modification"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to reload current playlist after modification"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# Not the current playlist, no need to reload
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reloading current playlist: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Global music player service instance
|
# Global music player service instance
|
||||||
|
|||||||
@@ -538,6 +538,7 @@ class StreamProcessingService:
|
|||||||
"""Add a sound to the main playlist."""
|
"""Add a sound to the main playlist."""
|
||||||
try:
|
try:
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
|
from app.services.music_player_service import music_player_service
|
||||||
|
|
||||||
# Find the main playlist
|
# Find the main playlist
|
||||||
main_playlist = Playlist.find_main_playlist()
|
main_playlist = Playlist.find_main_playlist()
|
||||||
@@ -546,6 +547,9 @@ class StreamProcessingService:
|
|||||||
# Add sound to the main playlist
|
# Add sound to the main playlist
|
||||||
main_playlist.add_sound(sound.id, commit=True)
|
main_playlist.add_sound(sound.id, commit=True)
|
||||||
logger.info(f"Added sound {sound.id} to main playlist")
|
logger.info(f"Added sound {sound.id} to main playlist")
|
||||||
|
|
||||||
|
# Reload the playlist in music player if it's the current one
|
||||||
|
music_player_service.reload_current_playlist_if_modified(main_playlist.id)
|
||||||
else:
|
else:
|
||||||
logger.warning("Main playlist not found - sound not added to any playlist")
|
logger.warning("Main playlist not found - sound not added to any playlist")
|
||||||
|
|
||||||
|
|||||||
7
main.py
7
main.py
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from app import create_app, socketio
|
from app import create_app, socketio
|
||||||
@@ -9,8 +10,8 @@ load_dotenv()
|
|||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
datefmt='%H:%M:%S'
|
datefmt="%H:%M:%S",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ def main() -> None:
|
|||||||
"""Run the Flask application with SocketIO."""
|
"""Run the Flask application with SocketIO."""
|
||||||
app = create_app()
|
app = create_app()
|
||||||
socketio.run(
|
socketio.run(
|
||||||
app, debug=True, host="0.0.0.0", port=5000, allow_unsafe_werkzeug=True
|
app, debug=True, host="127.0.0.1", port=5000, allow_unsafe_werkzeug=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user