refactor: Remove unused playlist routes and related logic; clean up sound and stream models

This commit is contained in:
JSC
2025-07-12 22:00:04 +02:00
parent 627b95c961
commit 688b95b6af
12 changed files with 34 additions and 1139 deletions

View File

@@ -100,7 +100,6 @@ def create_app():
auth, auth,
main, main,
player, player,
playlist,
soundboard, soundboard,
sounds, sounds,
stream, stream,
@@ -114,7 +113,6 @@ 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

View File

@@ -81,19 +81,6 @@ class Playlist(db.Model):
), ),
} }
def to_detailed_dict(self) -> dict:
"""Convert playlist to detailed dictionary with sounds."""
playlist_dict = self.to_dict()
playlist_dict["sounds"] = [
{
"sound": ps.sound.to_dict() if ps.sound else None,
"order": ps.order,
"added_at": ps.added_at.isoformat() if ps.added_at else None,
}
for ps in sorted(self.playlist_sounds, key=lambda x: x.order)
]
return playlist_dict
@classmethod @classmethod
def create_playlist( def create_playlist(
cls, cls,
@@ -123,26 +110,6 @@ class Playlist(db.Model):
return playlist return playlist
@classmethod
def find_by_name(
cls, name: str, user_id: Optional[int] = None
) -> Optional["Playlist"]:
"""Find playlist by name, optionally filtered by user."""
query = cls.query.filter_by(name=name)
if user_id is not None:
query = query.filter_by(user_id=user_id)
return query.first()
@classmethod
def find_by_user(cls, user_id: int) -> list["Playlist"]:
"""Find all playlists for a user."""
return cls.query.filter_by(user_id=user_id).order_by(cls.name).all()
@classmethod
def find_system_playlists(cls) -> list["Playlist"]:
"""Find all system playlists (user_id is None)."""
return cls.query.filter_by(user_id=None).order_by(cls.name).all()
@classmethod @classmethod
def find_current_playlist( def find_current_playlist(
cls, user_id: Optional[int] = None cls, user_id: Optional[int] = None
@@ -163,22 +130,6 @@ class Playlist(db.Model):
query = query.filter_by(user_id=user_id) query = query.filter_by(user_id=user_id)
return query.first() return query.first()
def set_as_current(self, commit: bool = True) -> None:
"""Set this playlist as the current one and unset others."""
# Unset other current playlists for the same user/system
if self.user_id is not None:
Playlist.query.filter_by(
user_id=self.user_id, is_current=True
).update({"is_current": False})
else:
Playlist.query.filter_by(user_id=None, is_current=True).update(
{"is_current": False}
)
self.is_current = True
if commit:
db.session.commit()
def add_sound( def add_sound(
self, sound_id: int, order: Optional[int] = None, commit: bool = True self, sound_id: int, order: Optional[int] = None, commit: bool = True
) -> "PlaylistSound": ) -> "PlaylistSound":
@@ -203,87 +154,3 @@ class Playlist(db.Model):
db.session.commit() db.session.commit()
return playlist_sound return playlist_sound
def remove_sound(self, sound_id: int, commit: bool = True) -> bool:
"""Remove a sound from the playlist."""
from app.models.playlist_sound import PlaylistSound
playlist_sound = PlaylistSound.query.filter_by(
playlist_id=self.id, sound_id=sound_id
).first()
if playlist_sound:
db.session.delete(playlist_sound)
if commit:
db.session.commit()
return True
return False
def reorder_sounds(
self, sound_orders: list[dict], commit: bool = True
) -> None:
"""Reorder sounds in the playlist.
Args:
sound_orders: List of dicts with 'sound_id' and 'order' keys
"""
from app.models.playlist_sound import PlaylistSound
for item in sound_orders:
playlist_sound = PlaylistSound.query.filter_by(
playlist_id=self.id, sound_id=item["sound_id"]
).first()
if playlist_sound:
playlist_sound.order = item["order"]
if commit:
db.session.commit()
def get_total_duration(self) -> int:
"""Get total duration of all sounds in the playlist in milliseconds."""
from app.models.sound import Sound
total = (
db.session.query(db.func.sum(Sound.duration))
.join(self.playlist_sounds)
.filter(Sound.id.in_([ps.sound_id for ps in self.playlist_sounds]))
.scalar()
)
return total or 0
def duplicate(
self, new_name: str, user_id: Optional[int] = None, commit: bool = True
) -> "Playlist":
"""Create a duplicate of this playlist."""
new_playlist = Playlist.create_playlist(
name=new_name,
description=self.description,
genre=self.genre,
user_id=user_id,
is_main=False,
is_deletable=True,
is_current=False,
commit=commit,
)
# Copy all sounds with their order
for ps in self.playlist_sounds:
new_playlist.add_sound(ps.sound_id, ps.order, commit=False)
if commit:
db.session.commit()
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()

View File

@@ -63,126 +63,3 @@ class PlaylistSound(db.Model):
"added_at": self.added_at.isoformat() if self.added_at else None, "added_at": self.added_at.isoformat() if self.added_at else None,
"sound": self.sound.to_dict() if self.sound else None, "sound": self.sound.to_dict() if self.sound else None,
} }
@classmethod
def create_playlist_sound(
cls,
playlist_id: int,
sound_id: int,
order: int,
commit: bool = True,
) -> "PlaylistSound":
"""Create a new playlist-sound relationship."""
playlist_sound = cls(
playlist_id=playlist_id,
sound_id=sound_id,
order=order,
)
db.session.add(playlist_sound)
if commit:
db.session.commit()
return playlist_sound
@classmethod
def find_by_playlist(cls, playlist_id: int) -> list["PlaylistSound"]:
"""Find all sounds in a playlist ordered by their position."""
return (
cls.query.filter_by(playlist_id=playlist_id)
.order_by(cls.order)
.all()
)
@classmethod
def find_by_sound(cls, sound_id: int) -> list["PlaylistSound"]:
"""Find all playlists containing a specific sound."""
return cls.query.filter_by(sound_id=sound_id).all()
@classmethod
def find_by_playlist_and_sound(
cls, playlist_id: int, sound_id: int
) -> Optional["PlaylistSound"]:
"""Find a specific playlist-sound relationship."""
return cls.query.filter_by(
playlist_id=playlist_id, sound_id=sound_id
).first()
@classmethod
def get_next_order(cls, playlist_id: int) -> int:
"""Get the next order number for a playlist."""
max_order = (
db.session.query(db.func.max(cls.order))
.filter_by(playlist_id=playlist_id)
.scalar()
)
return (max_order or 0) + 1
@classmethod
def reorder_playlist(
cls, playlist_id: int, sound_orders: list[dict], commit: bool = True
) -> None:
"""Reorder all sounds in a playlist.
Args:
playlist_id: ID of the playlist
sound_orders: List of dicts with 'sound_id' and 'order' keys
"""
for item in sound_orders:
playlist_sound = cls.query.filter_by(
playlist_id=playlist_id, sound_id=item["sound_id"]
).first()
if playlist_sound:
playlist_sound.order = item["order"]
if commit:
db.session.commit()
def move_to_position(self, new_order: int, commit: bool = True) -> None:
"""Move this sound to a new position in the playlist."""
old_order = self.order
if new_order == old_order:
return
# Get all other sounds in the playlist
other_sounds = (
PlaylistSound.query.filter_by(playlist_id=self.playlist_id)
.filter(PlaylistSound.id != self.id)
.order_by(PlaylistSound.order)
.all()
)
# Remove this sound from its current position
remaining_sounds = [ps for ps in other_sounds if ps.order != old_order]
# Insert at new position
if new_order <= len(remaining_sounds):
remaining_sounds.insert(new_order - 1, self)
else:
remaining_sounds.append(self)
# Update all order values
for i, ps in enumerate(remaining_sounds, 1):
ps.order = i
if commit:
db.session.commit()
def get_previous_sound(self) -> Optional["PlaylistSound"]:
"""Get the previous sound in the playlist."""
return (
PlaylistSound.query.filter_by(playlist_id=self.playlist_id)
.filter(PlaylistSound.order < self.order)
.order_by(PlaylistSound.order.desc())
.first()
)
def get_next_sound(self) -> Optional["PlaylistSound"]:
"""Get the next sound in the playlist."""
return (
PlaylistSound.query.filter_by(playlist_id=self.playlist_id)
.filter(PlaylistSound.order > self.order)
.order_by(PlaylistSound.order.asc())
.first()
)

View File

@@ -197,21 +197,6 @@ class Sound(db.Model):
"""Find all sounds by type.""" """Find all sounds by type."""
return cls.query.filter_by(type=sound_type).all() return cls.query.filter_by(type=sound_type).all()
@classmethod
def get_most_played(cls, limit: int = 10) -> list["Sound"]:
"""Get the most played sounds."""
return cls.query.order_by(cls.play_count.desc()).limit(limit).all()
@classmethod
def get_music_sounds(cls) -> list["Sound"]:
"""Get all music sounds."""
return cls.query.filter_by(is_music=True).all()
@classmethod
def get_deletable_sounds(cls) -> list["Sound"]:
"""Get all deletable sounds."""
return cls.query.filter_by(is_deletable=True).all()
@classmethod @classmethod
def create_sound( def create_sound(
cls, cls,

View File

@@ -92,173 +92,3 @@ class SoundPlayed(db.Model):
if commit: if commit:
db.session.commit() db.session.commit()
return play_record return play_record
@classmethod
def get_user_plays(
cls,
user_id: int,
limit: int = 50,
offset: int = 0,
) -> list["SoundPlayed"]:
"""Get recent plays for a specific user."""
return (
cls.query.filter_by(user_id=user_id)
.order_by(cls.played_at.desc())
.offset(offset)
.limit(limit)
.all()
)
@classmethod
def get_sound_plays(
cls,
sound_id: int,
limit: int = 50,
offset: int = 0,
) -> list["SoundPlayed"]:
"""Get recent plays for a specific sound."""
return (
cls.query.filter_by(sound_id=sound_id)
.order_by(cls.played_at.desc())
.offset(offset)
.limit(limit)
.all()
)
@classmethod
def get_recent_plays(
cls,
limit: int = 100,
offset: int = 0,
) -> list["SoundPlayed"]:
"""Get recent plays across all users and sounds."""
return (
cls.query.order_by(cls.played_at.desc())
.offset(offset)
.limit(limit)
.all()
)
@classmethod
def get_user_play_count(cls, user_id: int) -> int:
"""Get total play count for a user."""
return cls.query.filter_by(user_id=user_id).count()
@classmethod
def get_sound_play_count(cls, sound_id: int) -> int:
"""Get total play count for a sound."""
return cls.query.filter_by(sound_id=sound_id).count()
@classmethod
def get_popular_sounds(
cls,
limit: int = 10,
days: int | None = None,
) -> list[dict]:
"""Get most popular sounds with play counts."""
from app.models.sound import Sound
query = (
db.session.query(
cls.sound_id,
func.count(cls.id).label("play_count"),
func.max(cls.played_at).label("last_played"),
)
.group_by(cls.sound_id)
.order_by(func.count(cls.id).desc())
)
if days:
query = query.filter(
cls.played_at >= text(f"datetime('now', '-{days} days')"),
)
results = query.limit(limit).all()
# Get sound details
popular_sounds = []
for result in results:
sound = Sound.query.get(result.sound_id)
if sound:
popular_sounds.append(
{
"sound": sound.to_dict(),
"play_count": result.play_count,
"last_played": (
result.last_played.isoformat()
if result.last_played
else None
),
},
)
return popular_sounds
@classmethod
def get_user_stats(cls, user_id: int) -> dict:
"""Get comprehensive stats for a user."""
from app.models.sound import Sound
total_plays = cls.query.filter_by(user_id=user_id).count()
if total_plays == 0:
return {
"total_plays": 0,
"unique_sounds": 0,
"favorite_sound": None,
"first_play": None,
"last_play": None,
}
# Get unique sounds count
unique_sounds = (
db.session.query(cls.sound_id)
.filter_by(user_id=user_id)
.distinct()
.count()
)
# Get favorite sound
favorite_query = (
db.session.query(
cls.sound_id,
func.count(cls.id).label("play_count"),
)
.filter_by(user_id=user_id)
.group_by(cls.sound_id)
.order_by(func.count(cls.id).desc())
.first()
)
favorite_sound = None
if favorite_query:
sound = Sound.query.get(favorite_query.sound_id)
if sound:
favorite_sound = {
"sound": sound.to_dict(),
"play_count": favorite_query.play_count,
}
# Get first and last play dates
first_play = (
cls.query.filter_by(user_id=user_id)
.order_by(cls.played_at.asc())
.first()
)
last_play = (
cls.query.filter_by(user_id=user_id)
.order_by(cls.played_at.desc())
.first()
)
return {
"total_plays": total_plays,
"unique_sounds": unique_sounds,
"favorite_sound": favorite_sound,
"first_play": (
first_play.played_at.isoformat() if first_play else None
),
"last_play": (
last_play.played_at.isoformat() if last_play else None
),
}

View File

@@ -4,7 +4,14 @@ from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy import (
DateTime,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import db from app.database import db
@@ -51,9 +58,7 @@ class Stream(db.Model):
# Constraints # Constraints
__table_args__ = ( __table_args__ = (
UniqueConstraint( UniqueConstraint("service", "service_id", name="unique_service_stream"),
"service", "service_id", name="unique_service_stream"
),
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -117,70 +122,3 @@ class Stream(db.Model):
db.session.commit() db.session.commit()
return stream return stream
@classmethod
def find_by_service_and_id(
cls, service: str, service_id: str
) -> Optional["Stream"]:
"""Find stream by service and service_id."""
return cls.query.filter_by(
service=service, service_id=service_id
).first()
@classmethod
def find_by_sound(cls, sound_id: int) -> list["Stream"]:
"""Find all streams for a specific sound."""
return cls.query.filter_by(sound_id=sound_id).all()
@classmethod
def find_by_service(cls, service: str) -> list["Stream"]:
"""Find all streams for a specific service."""
return cls.query.filter_by(service=service).all()
@classmethod
def find_by_status(cls, status: str) -> list["Stream"]:
"""Find all streams with a specific status."""
return cls.query.filter_by(status=status).all()
@classmethod
def find_active_streams(cls) -> list["Stream"]:
"""Find all active streams."""
return cls.query.filter_by(status="active").all()
def update_metadata(
self,
title: Optional[str] = None,
track: Optional[str] = None,
artist: Optional[str] = None,
album: Optional[str] = None,
genre: Optional[str] = None,
commit: bool = True,
) -> None:
"""Update stream metadata."""
if title is not None:
self.title = title
if track is not None:
self.track = track
if artist is not None:
self.artist = artist
if album is not None:
self.album = album
if genre is not None:
self.genre = genre
if commit:
db.session.commit()
def set_status(self, status: str, commit: bool = True) -> None:
"""Update stream status."""
self.status = status
if commit:
db.session.commit()
def is_active(self) -> bool:
"""Check if stream is active."""
return self.status == "active"
def get_display_name(self) -> str:
"""Get a display name for the stream (title or track or service_id)."""
return self.title or self.track or self.service_id

View File

@@ -82,53 +82,3 @@ def check_ffmpeg():
return jsonify(ffmpeg_status), 200 return jsonify(ffmpeg_status), 200
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@bp.route("/list", methods=["GET"])
@require_admin
def list_sounds():
"""Get detailed list of all sounds with normalization status."""
from app.services.sound_management_service import SoundManagementService
return ErrorHandlingService.wrap_service_call(
SoundManagementService.get_sounds_with_file_status,
request.args.get("type", "SDB"),
int(request.args.get("page", 1)),
int(request.args.get("per_page", 50)),
)
@bp.route("/<int:sound_id>", methods=["DELETE"])
@require_admin
def delete_sound(sound_id: int):
"""Delete a sound and its files."""
from app.services.sound_management_service import SoundManagementService
return ErrorHandlingService.wrap_service_call(
SoundManagementService.delete_sound_with_files,
sound_id,
)
@bp.route("/<int:sound_id>/normalize", methods=["POST"])
@require_admin
def normalize_single_sound(sound_id: int):
"""Normalize a specific sound."""
try:
from app.services.sound_management_service import SoundManagementService
data = request.get_json() or {}
overwrite = data.get("overwrite", False)
two_pass = data.get("two_pass", True)
result = SoundManagementService.normalize_sound(
sound_id,
overwrite,
two_pass,
)
if result["success"]:
return jsonify(result), 200
return jsonify(result), 400
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -2,71 +2,10 @@
from flask import Blueprint from flask import Blueprint
from app.services.decorators import (
get_current_user,
require_auth,
require_credits,
)
bp = Blueprint("main", __name__) bp = Blueprint("main", __name__)
@bp.route("/")
def index() -> dict[str, str]:
"""Root endpoint that returns API status."""
return {"message": "API is running", "status": "ok"}
@bp.route("/protected")
@require_auth
def protected() -> dict[str, str]:
"""Protected endpoint that requires authentication."""
user = get_current_user()
return {
"message": f"Hello {user['name']}, this is a protected endpoint!",
"user": user,
}
@bp.route("/api-protected")
@require_auth
def api_protected() -> dict[str, str]:
"""Protected endpoint that accepts JWT or API token authentication."""
user = get_current_user()
return {
"message": f"Hello {user['name']}, you accessed this via {user['provider']}!",
"user": user,
}
@bp.route("/health") @bp.route("/health")
def health() -> dict[str, str]: def health() -> dict[str, str]:
"""Health check endpoint.""" """Health check endpoint."""
return {"status": "ok"} return {"status": "ok"}
@bp.route("/use-credits/<int:amount>")
@require_auth
@require_credits(5)
def use_credits(amount: int) -> dict[str, str]:
"""Test endpoint that costs 5 credits to use."""
user = get_current_user()
return {
"message": f"Successfully used endpoint! You requested amount: {amount}",
"user": user["email"],
"remaining_credits": user["credits"]
- 5, # Note: credits already deducted by decorator
}
@bp.route("/expensive-operation")
@require_auth
@require_credits(10)
def expensive_operation() -> dict[str, str]:
"""Test endpoint that costs 10 credits to use."""
user = get_current_user()
return {
"message": "Expensive operation completed successfully!",
"user": user["email"],
"operation_cost": 10,
}

View File

@@ -93,11 +93,14 @@ def seek():
data = request.get_json() data = request.get_json()
if not data or "position" not in data: if not data or "position" not in data:
return jsonify({"error": "Position required"}), 400 return jsonify({"error": "Position required"}), 400
position = float(data["position"]) position = float(data["position"])
if not 0.0 <= position <= 1.0: if not 0.0 <= position <= 1.0:
return jsonify({"error": "Position must be between 0.0 and 1.0"}), 400 return (
jsonify({"error": "Position must be between 0.0 and 1.0"}),
400,
)
success = music_player_service.seek(position) success = music_player_service.seek(position)
if success: if success:
return jsonify({"message": "Seek successful"}), 200 return jsonify({"message": "Seek successful"}), 200
@@ -116,11 +119,11 @@ def set_volume():
data = request.get_json() data = request.get_json()
if not data or "volume" not in data: if not data or "volume" not in data:
return jsonify({"error": "Volume required"}), 400 return jsonify({"error": "Volume required"}), 400
volume = int(data["volume"]) volume = int(data["volume"])
if not 0 <= volume <= 100: if not 0 <= volume <= 100:
return jsonify({"error": "Volume must be between 0 and 100"}), 400 return jsonify({"error": "Volume must be between 0 and 100"}), 400
success = music_player_service.set_volume(volume) success = music_player_service.set_volume(volume)
if success: if success:
return jsonify({"message": "Volume set successfully"}), 200 return jsonify({"message": "Volume set successfully"}), 200
@@ -139,12 +142,23 @@ def set_play_mode():
data = request.get_json() data = request.get_json()
if not data or "mode" not in data: if not data or "mode" not in data:
return jsonify({"error": "Mode required"}), 400 return jsonify({"error": "Mode required"}), 400
mode = data["mode"] mode = data["mode"]
valid_modes = ["continuous", "loop-playlist", "loop-one", "random", "single"] valid_modes = [
"continuous",
"loop-playlist",
"loop-one",
"random",
"single",
]
if mode not in valid_modes: if mode not in valid_modes:
return jsonify({"error": f"Mode must be one of: {', '.join(valid_modes)}"}), 400 return (
jsonify(
{"error": f"Mode must be one of: {', '.join(valid_modes)}"}
),
400,
)
success = music_player_service.set_play_mode(mode) success = music_player_service.set_play_mode(mode)
if success: if success:
return jsonify({"message": "Play mode set successfully"}), 200 return jsonify({"message": "Play mode set successfully"}), 200
@@ -161,7 +175,7 @@ def load_playlist():
data = request.get_json() data = request.get_json()
if not data or "playlist_id" not in data: if not data or "playlist_id" not in data:
return jsonify({"error": "Playlist ID required"}), 400 return jsonify({"error": "Playlist ID required"}), 400
playlist_id = int(data["playlist_id"]) playlist_id = int(data["playlist_id"])
success = music_player_service.load_playlist(playlist_id) success = music_player_service.load_playlist(playlist_id)
if success: if success:
@@ -181,7 +195,7 @@ def play_track():
data = request.get_json() data = request.get_json()
if not data or "index" not in data: if not data or "index" not in data:
return jsonify({"error": "Track index required"}), 400 return jsonify({"error": "Track index required"}), 400
index = int(data["index"]) index = int(data["index"])
success = music_player_service.play_track_at_index(index) success = music_player_service.play_track_at_index(index)
if success: if success:
@@ -191,42 +205,3 @@ def play_track():
return jsonify({"error": "Invalid track index"}), 400 return jsonify({"error": "Invalid track index"}), 400
except Exception as e: except Exception as e:
return ErrorHandlingService.handle_generic_error(e) return ErrorHandlingService.handle_generic_error(e)
@bp.route("/start-instance", methods=["POST"])
@require_auth
def start_vlc_instance():
"""Start the VLC player instance."""
try:
success = music_player_service.start_vlc_instance()
if success:
return jsonify({"message": "VLC instance started successfully"}), 200
return jsonify({"error": "Failed to start VLC instance"}), 500
except Exception as e:
return ErrorHandlingService.handle_generic_error(e)
@bp.route("/stop-instance", methods=["POST"])
@require_auth
def stop_vlc_instance():
"""Stop the VLC player instance."""
try:
success = music_player_service.stop_vlc_instance()
if success:
return jsonify({"message": "VLC instance stopped successfully"}), 200
return jsonify({"error": "Failed to stop VLC instance"}), 500
except Exception as e:
return ErrorHandlingService.handle_generic_error(e)
@bp.route("/test-emit", methods=["POST"])
@require_auth
def test_emit():
"""Test SocketIO emission manually."""
try:
# Force emit player state
music_player_service._emit_player_state()
return jsonify({"message": "Test emission sent"}), 200
except Exception as e:
return ErrorHandlingService.handle_generic_error(e)

View File

@@ -1,250 +0,0 @@
"""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

View File

@@ -140,99 +140,3 @@ def get_status():
) )
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@bp.route("/history", methods=["GET"])
@require_auth
def get_play_history():
"""Get recent play history."""
try:
page = int(request.args.get("page", 1))
per_page = min(int(request.args.get("per_page", 50)), 100)
offset = (page - 1) * per_page
recent_plays = SoundPlayed.get_recent_plays(
limit=per_page,
offset=offset,
)
return jsonify(
{
"plays": [play.to_dict() for play in recent_plays],
"page": page,
"per_page": per_page,
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/my-history", methods=["GET"])
@require_auth
def get_my_play_history():
"""Get current user's play history."""
try:
user = get_current_user()
if not user:
return jsonify({"error": "User not found"}), 404
user_id = int(user["id"])
page = int(request.args.get("page", 1))
per_page = min(int(request.args.get("per_page", 50)), 100)
offset = (page - 1) * per_page
user_plays = SoundPlayed.get_user_plays(
user_id=user_id,
limit=per_page,
offset=offset,
)
return jsonify(
{
"plays": [play.to_dict() for play in user_plays],
"page": page,
"per_page": per_page,
"user_id": user_id,
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/my-stats", methods=["GET"])
@require_auth
def get_my_stats():
"""Get current user's play statistics."""
try:
user = get_current_user()
if not user:
return jsonify({"error": "User not found"}), 404
user_id = int(user["id"])
stats = SoundPlayed.get_user_stats(user_id)
return jsonify(stats)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/popular", methods=["GET"])
@require_auth
def get_popular_sounds():
"""Get most popular sounds."""
try:
limit = min(int(request.args.get("limit", 10)), 50)
days = request.args.get("days")
days = int(days) if days and days.isdigit() else None
popular_sounds = SoundPlayed.get_popular_sounds(limit=limit, days=days)
return jsonify(
{
"popular_sounds": popular_sounds,
"limit": limit,
"days": days,
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -90,121 +90,3 @@ def add_url():
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@bp.route("/", methods=["GET"])
@require_auth
def list_streams():
"""List all streams with optional filtering."""
try:
status = request.args.get("status")
service = request.args.get("service")
query = Stream.query
if status:
query = query.filter_by(status=status)
if service:
query = query.filter_by(service=service)
streams = query.order_by(Stream.created_at.desc()).all()
return (
jsonify({"streams": [stream.to_dict() for stream in streams]}),
200,
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<int:stream_id>", methods=["GET"])
@require_auth
def get_stream(stream_id):
"""Get a specific stream by ID."""
try:
stream = Stream.query.get_or_404(stream_id)
return jsonify({"stream": stream.to_dict()}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/<int:stream_id>", methods=["PUT"])
@require_auth
def update_stream(stream_id):
"""Update stream metadata."""
try:
stream = Stream.query.get_or_404(stream_id)
data = request.get_json()
if not data:
return jsonify({"error": "No data provided"}), 400
# Update allowed fields
updatable_fields = [
"title",
"track",
"artist",
"album",
"genre",
"status",
]
for field in updatable_fields:
if field in data:
setattr(stream, field, data[field])
db.session.commit()
return (
jsonify(
{
"message": "Stream updated successfully",
"stream": stream.to_dict(),
}
),
200,
)
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@bp.route("/<int:stream_id>", methods=["DELETE"])
@require_auth
def delete_stream(stream_id):
"""Delete a stream."""
try:
stream = Stream.query.get_or_404(stream_id)
# If stream is being processed, mark for deletion instead
if stream.status == "processing":
stream.status = "cancelled"
db.session.commit()
return jsonify({"message": "Stream marked for cancellation"}), 200
db.session.delete(stream)
db.session.commit()
return jsonify({"message": "Stream deleted successfully"}), 200
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
@bp.route("/queue/status", methods=["GET"])
@require_auth
def queue_status():
"""Get the current processing queue status."""
try:
from app.services.stream_processing_service import (
StreamProcessingService,
)
status = StreamProcessingService.get_queue_status()
return jsonify(status), 200
except Exception as e:
return jsonify({"error": str(e)}), 500