Compare commits
6 Commits
c3b8205f83
...
2f7ffbbfe4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7ffbbfe4 | ||
|
|
5876b247f4 | ||
|
|
ccc5ee38e2 | ||
|
|
1cd43a670d | ||
|
|
4375718c2f | ||
|
|
5c29fa1a4c |
@@ -4,13 +4,15 @@ from datetime import timedelta
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from flask_jwt_extended import JWTManager
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from app.database import init_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.scheduler_service import scheduler_service
|
||||
|
||||
# Global auth service instance
|
||||
# Global service instances
|
||||
auth_service = AuthService()
|
||||
socketio = SocketIO()
|
||||
|
||||
|
||||
def create_app():
|
||||
@@ -35,7 +37,7 @@ def create_app():
|
||||
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
|
||||
app.config["JWT_COOKIE_SECURE"] = False # Set to True in production
|
||||
app.config["JWT_COOKIE_CSRF_PROTECT"] = False
|
||||
app.config["JWT_ACCESS_COOKIE_PATH"] = "/api/"
|
||||
app.config["JWT_ACCESS_COOKIE_PATH"] = "/" # Allow access to all paths including SocketIO
|
||||
app.config["JWT_REFRESH_COOKIE_PATH"] = "/api/auth/refresh"
|
||||
|
||||
# Initialize CORS
|
||||
@@ -47,6 +49,13 @@ def create_app():
|
||||
methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
||||
)
|
||||
|
||||
# Initialize SocketIO
|
||||
socketio.init_app(
|
||||
app,
|
||||
cors_allowed_origins="http://localhost:3000",
|
||||
cors_credentials=True,
|
||||
)
|
||||
|
||||
# Initialize JWT manager
|
||||
jwt = JWTManager(app)
|
||||
|
||||
@@ -62,6 +71,9 @@ def create_app():
|
||||
# Initialize authentication service with app
|
||||
auth_service.init_app(app)
|
||||
|
||||
# Initialize SocketIO service (import after socketio is initialized)
|
||||
from app.services.socketio_service import socketio_service # noqa: F401
|
||||
|
||||
# Initialize scheduler service with app
|
||||
scheduler_service.app = app
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app.database import db
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import db
|
||||
|
||||
|
||||
class SoundType(Enum):
|
||||
"""Sound type enumeration."""
|
||||
@@ -75,13 +77,13 @@ class Sound(db.Model):
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@@ -114,7 +116,7 @@ class Sound(db.Model):
|
||||
def increment_play_count(self) -> None:
|
||||
"""Increment the play count for this sound."""
|
||||
self.play_count += 1
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
db.session.commit()
|
||||
|
||||
def set_normalized_info(
|
||||
@@ -130,7 +132,7 @@ class Sound(db.Model):
|
||||
self.normalized_size = normalized_size
|
||||
self.normalized_hash = normalized_hash
|
||||
self.is_normalized = True
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
def clear_normalized_info(self) -> None:
|
||||
"""Clear normalized sound information."""
|
||||
@@ -139,7 +141,7 @@ class Sound(db.Model):
|
||||
self.normalized_hash = None
|
||||
self.normalized_size = None
|
||||
self.is_normalized = False
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
def update_file_info(
|
||||
self,
|
||||
@@ -153,7 +155,7 @@ class Sound(db.Model):
|
||||
self.duration = duration
|
||||
self.size = size
|
||||
self.hash = hash_value
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
@classmethod
|
||||
def find_by_hash(cls, hash_value: str) -> Optional["Sound"]:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Sound played tracking model."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import db
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
||||
class SoundPlayed(db.Model):
|
||||
@@ -17,19 +18,21 @@ class SoundPlayed(db.Model):
|
||||
|
||||
# Foreign keys
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False
|
||||
Integer,
|
||||
ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
)
|
||||
sound_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("sounds.id"), nullable=False
|
||||
Integer,
|
||||
ForeignKey("sounds.id"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Additional context
|
||||
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
|
||||
user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
|
||||
# Timestamp
|
||||
played_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, nullable=False
|
||||
DateTime,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# Relationships
|
||||
@@ -37,8 +40,11 @@ class SoundPlayed(db.Model):
|
||||
sound: Mapped["Sound"] = relationship("Sound", backref="play_history")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of SoundPlayed."""
|
||||
return f"<SoundPlayed user_id={self.user_id} sound_id={self.sound_id} at={self.played_at}>"
|
||||
"""Return string representation of SoundPlayed."""
|
||||
return (
|
||||
f"<SoundPlayed user_id={self.user_id} sound_id={self.sound_id} "
|
||||
f"at={self.played_at}>"
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert sound played record to dictionary."""
|
||||
@@ -46,20 +52,26 @@ class SoundPlayed(db.Model):
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"sound_id": self.sound_id,
|
||||
"ip_address": self.ip_address,
|
||||
"user_agent": self.user_agent,
|
||||
"played_at": self.played_at.isoformat(),
|
||||
"user": {
|
||||
"user": (
|
||||
{
|
||||
"id": self.user.id,
|
||||
"name": self.user.name,
|
||||
"email": self.user.email,
|
||||
} if self.user else None,
|
||||
"sound": {
|
||||
}
|
||||
if self.user
|
||||
else None
|
||||
),
|
||||
"sound": (
|
||||
{
|
||||
"id": self.sound.id,
|
||||
"name": self.sound.name,
|
||||
"filename": self.sound.filename,
|
||||
"type": self.sound.type,
|
||||
} if self.sound else None,
|
||||
}
|
||||
if self.sound
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -67,16 +79,13 @@ class SoundPlayed(db.Model):
|
||||
cls,
|
||||
user_id: int,
|
||||
sound_id: int,
|
||||
ip_address: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
*,
|
||||
commit: bool = True,
|
||||
) -> "SoundPlayed":
|
||||
"""Create a new sound played record."""
|
||||
play_record = cls(
|
||||
user_id=user_id,
|
||||
sound_id=sound_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
|
||||
db.session.add(play_record)
|
||||
@@ -86,8 +95,11 @@ class SoundPlayed(db.Model):
|
||||
|
||||
@classmethod
|
||||
def get_user_plays(
|
||||
cls, user_id: int, limit: int = 50, offset: int = 0
|
||||
) -> List["SoundPlayed"]:
|
||||
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)
|
||||
@@ -99,8 +111,11 @@ class SoundPlayed(db.Model):
|
||||
|
||||
@classmethod
|
||||
def get_sound_plays(
|
||||
cls, sound_id: int, limit: int = 50, offset: int = 0
|
||||
) -> List["SoundPlayed"]:
|
||||
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)
|
||||
@@ -112,8 +127,10 @@ class SoundPlayed(db.Model):
|
||||
|
||||
@classmethod
|
||||
def get_recent_plays(
|
||||
cls, limit: int = 100, offset: int = 0
|
||||
) -> List["SoundPlayed"]:
|
||||
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())
|
||||
@@ -134,10 +151,12 @@ class SoundPlayed(db.Model):
|
||||
|
||||
@classmethod
|
||||
def get_popular_sounds(
|
||||
cls, limit: int = 10, days: int | None = None
|
||||
) -> List[dict]:
|
||||
cls,
|
||||
limit: int = 10,
|
||||
days: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""Get most popular sounds with play counts."""
|
||||
from sqlalchemy import func, text
|
||||
from app.models.sound import Sound
|
||||
|
||||
query = (
|
||||
db.session.query(
|
||||
@@ -151,7 +170,7 @@ class SoundPlayed(db.Model):
|
||||
|
||||
if days:
|
||||
query = query.filter(
|
||||
cls.played_at >= text(f"datetime('now', '-{days} days')")
|
||||
cls.played_at >= text(f"datetime('now', '-{days} days')"),
|
||||
)
|
||||
|
||||
results = query.limit(limit).all()
|
||||
@@ -159,22 +178,26 @@ class SoundPlayed(db.Model):
|
||||
# Get sound details
|
||||
popular_sounds = []
|
||||
for result in results:
|
||||
from app.models.sound import Sound
|
||||
|
||||
sound = Sound.query.get(result.sound_id)
|
||||
if sound:
|
||||
popular_sounds.append({
|
||||
popular_sounds.append(
|
||||
{
|
||||
"sound": sound.to_dict(),
|
||||
"play_count": result.play_count,
|
||||
"last_played": result.last_played.isoformat() if result.last_played else None,
|
||||
})
|
||||
"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 sqlalchemy import func
|
||||
from app.models.sound import Sound
|
||||
|
||||
total_plays = cls.query.filter_by(user_id=user_id).count()
|
||||
|
||||
@@ -198,7 +221,8 @@ class SoundPlayed(db.Model):
|
||||
# Get favorite sound
|
||||
favorite_query = (
|
||||
db.session.query(
|
||||
cls.sound_id, func.count(cls.id).label("play_count")
|
||||
cls.sound_id,
|
||||
func.count(cls.id).label("play_count"),
|
||||
)
|
||||
.filter_by(user_id=user_id)
|
||||
.group_by(cls.sound_id)
|
||||
@@ -208,8 +232,6 @@ class SoundPlayed(db.Model):
|
||||
|
||||
favorite_sound = None
|
||||
if favorite_query:
|
||||
from app.models.sound import Sound
|
||||
|
||||
sound = Sound.query.get(favorite_query.sound_id)
|
||||
if sound:
|
||||
favorite_sound = {
|
||||
@@ -233,6 +255,10 @@ class SoundPlayed(db.Model):
|
||||
"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,
|
||||
"first_play": (
|
||||
first_play.played_at.isoformat() if first_play else None
|
||||
),
|
||||
"last_play": (
|
||||
last_play.played_at.isoformat() if last_play else None
|
||||
),
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -63,13 +64,13 @@ class User(db.Model):
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@@ -103,9 +104,11 @@ class User(db.Model):
|
||||
"role": self.role,
|
||||
"is_active": self.is_active,
|
||||
"api_token": self.api_token,
|
||||
"api_token_expires_at": self.api_token_expires_at.isoformat()
|
||||
"api_token_expires_at": (
|
||||
self.api_token_expires_at.isoformat()
|
||||
if self.api_token_expires_at
|
||||
else None,
|
||||
else None
|
||||
),
|
||||
"providers": providers,
|
||||
"plan": self.plan.to_dict() if self.plan else None,
|
||||
"credits": self.credits,
|
||||
@@ -129,13 +132,13 @@ class User(db.Model):
|
||||
self.email = provider_data.get("email", self.email)
|
||||
self.name = provider_data.get("name", self.name)
|
||||
self.picture = provider_data.get("picture", self.picture)
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
db.session.commit()
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
"""Hash and set user password."""
|
||||
self.password_hash = generate_password_hash(password)
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
def check_password(self, password: str) -> bool:
|
||||
"""Check if provided password matches user's password."""
|
||||
@@ -151,7 +154,7 @@ class User(db.Model):
|
||||
"""Generate a new API token for the user."""
|
||||
self.api_token = secrets.token_urlsafe(32)
|
||||
self.api_token_expires_at = None # No expiration by default
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
return self.api_token
|
||||
|
||||
def is_api_token_valid(self) -> bool:
|
||||
@@ -162,23 +165,23 @@ class User(db.Model):
|
||||
if self.api_token_expires_at is None:
|
||||
return True # No expiration
|
||||
|
||||
return datetime.utcnow() < self.api_token_expires_at
|
||||
return datetime.now(tz=ZoneInfo("UTC")) < self.api_token_expires_at
|
||||
|
||||
def revoke_api_token(self) -> None:
|
||||
"""Revoke the user's API token."""
|
||||
self.api_token = None
|
||||
self.api_token_expires_at = None
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Activate the user account."""
|
||||
self.is_active = True
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Deactivate the user account."""
|
||||
self.is_active = False
|
||||
self.updated_at = datetime.utcnow()
|
||||
self.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
@classmethod
|
||||
def find_by_email(cls, email: str) -> Optional["User"]:
|
||||
@@ -218,7 +221,7 @@ class User(db.Model):
|
||||
oauth_provider.email = email
|
||||
oauth_provider.name = name
|
||||
oauth_provider.picture = picture
|
||||
oauth_provider.updated_at = datetime.utcnow()
|
||||
oauth_provider.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
|
||||
# Update user info with latest data
|
||||
user.update_from_provider(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -34,13 +35,13 @@ class UserOAuth(db.Model):
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime,
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
default=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@@ -107,7 +108,7 @@ class UserOAuth(db.Model):
|
||||
oauth_provider.email = email
|
||||
oauth_provider.name = name
|
||||
oauth_provider.picture = picture
|
||||
oauth_provider.updated_at = datetime.utcnow()
|
||||
oauth_provider.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
else:
|
||||
# Create new provider
|
||||
oauth_provider = cls(
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
"""Soundboard routes."""
|
||||
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from app.models.sound import Sound, SoundType
|
||||
from app.models.sound_played import SoundPlayed
|
||||
from app.services.decorators import (
|
||||
get_current_user,
|
||||
require_auth,
|
||||
require_credits,
|
||||
)
|
||||
from app.services.vlc_service import vlc_service
|
||||
from app.services.decorators import require_auth, get_current_user
|
||||
|
||||
bp = Blueprint("soundboard", __name__, url_prefix="/api/soundboard")
|
||||
|
||||
@@ -40,6 +45,7 @@ def get_sounds():
|
||||
|
||||
@bp.route("/sounds/<int:sound_id>/play", methods=["POST"])
|
||||
@require_auth
|
||||
@require_credits(1)
|
||||
def play_sound(sound_id: int):
|
||||
"""Play a specific sound."""
|
||||
try:
|
||||
@@ -61,9 +67,10 @@ def play_sound(sound_id: int):
|
||||
if success:
|
||||
return jsonify({"message": "Sound playing", "sound_id": sound_id})
|
||||
else:
|
||||
return jsonify(
|
||||
{"error": "Sound not found or cannot be played"}
|
||||
), 404
|
||||
return (
|
||||
jsonify({"error": "Sound not found or cannot be played"}),
|
||||
404,
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -78,15 +85,18 @@ def stop_all_sounds():
|
||||
|
||||
# Wait a moment and check if any are still playing
|
||||
import time
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
# If there are still instances, force stop them
|
||||
if vlc_service.get_playing_count() > 0:
|
||||
stopped_count = vlc_service.force_stop_all()
|
||||
return jsonify({
|
||||
return jsonify(
|
||||
{
|
||||
"message": f"Force stopped {stopped_count} sounds",
|
||||
"forced": True
|
||||
})
|
||||
"forced": True,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({"message": "All sounds stopped"})
|
||||
except Exception as e:
|
||||
@@ -99,10 +109,12 @@ def force_stop_all_sounds():
|
||||
"""Force stop all sounds with aggressive cleanup."""
|
||||
try:
|
||||
stopped_count = vlc_service.force_stop_all()
|
||||
return jsonify({
|
||||
return jsonify(
|
||||
{
|
||||
"message": f"Force stopped {stopped_count} sound instances",
|
||||
"stopped_count": stopped_count
|
||||
})
|
||||
"stopped_count": stopped_count,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -118,11 +130,13 @@ def get_status():
|
||||
with vlc_service.lock:
|
||||
processes = []
|
||||
for process_id, process in vlc_service.processes.items():
|
||||
processes.append({
|
||||
processes.append(
|
||||
{
|
||||
"id": process_id,
|
||||
"pid": process.pid,
|
||||
"running": process.poll() is None,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
@@ -144,13 +158,17 @@ def get_play_history():
|
||||
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)
|
||||
recent_plays = SoundPlayed.get_recent_plays(
|
||||
limit=per_page, offset=offset
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
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
|
||||
|
||||
@@ -169,14 +187,18 @@ def get_my_play_history():
|
||||
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)
|
||||
user_plays = SoundPlayed.get_user_plays(
|
||||
user_id=user_id, limit=per_page, offset=offset
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
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
|
||||
|
||||
@@ -209,10 +231,12 @@ def get_popular_sounds():
|
||||
|
||||
popular_sounds = SoundPlayed.get_popular_sounds(limit=limit, days=days)
|
||||
|
||||
return jsonify({
|
||||
return jsonify(
|
||||
{
|
||||
"popular_sounds": popular_sounds,
|
||||
"limit": limit,
|
||||
"days": days,
|
||||
})
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app.database import db
|
||||
from app.models.user import User
|
||||
@@ -62,7 +63,7 @@ class CreditService:
|
||||
|
||||
if credits_added > 0:
|
||||
user.credits = new_credits
|
||||
user.updated_at = datetime.utcnow()
|
||||
user.updated_at = datetime.now(tz=ZoneInfo("UTC"))
|
||||
total_credits_added += credits_added
|
||||
|
||||
logger.debug(
|
||||
|
||||
@@ -148,22 +148,7 @@ def require_role(required_role: str):
|
||||
|
||||
def require_admin(f):
|
||||
"""Decorator to require admin role for routes."""
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return jsonify({"error": "Authentication required"}), 401
|
||||
|
||||
if user.get("role") != "admin":
|
||||
return (
|
||||
jsonify({"error": "Access denied. Admin role required"}),
|
||||
403,
|
||||
)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return require_role("admin")(f)
|
||||
|
||||
|
||||
def require_credits(credits_needed: int):
|
||||
@@ -200,6 +185,16 @@ def require_credits(credits_needed: int):
|
||||
user.credits -= credits_needed
|
||||
db.session.commit()
|
||||
|
||||
# Emit credits changed event via SocketIO
|
||||
try:
|
||||
from app.services.socketio_service import socketio_service
|
||||
socketio_service.emit_credits_changed(user.id, user.credits)
|
||||
except Exception as e:
|
||||
# Don't fail the request if SocketIO emission fails
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to emit credits_changed event: {e}")
|
||||
|
||||
# Execute the function
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
|
||||
134
app/services/socketio_service.py
Normal file
134
app/services/socketio_service.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""SocketIO service for real-time communication."""
|
||||
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_jwt_extended import decode_token
|
||||
from flask_socketio import disconnect, emit, join_room, leave_room
|
||||
|
||||
from app import socketio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SocketIOService:
|
||||
"""Service for managing SocketIO connections and user rooms."""
|
||||
|
||||
@staticmethod
|
||||
def get_user_room(user_id: int) -> str:
|
||||
"""Get the room name for a specific user."""
|
||||
return f"user_{user_id}"
|
||||
|
||||
@staticmethod
|
||||
def emit_to_user(user_id: int, event: str, data: dict) -> None:
|
||||
"""Emit an event to a specific user's room."""
|
||||
room = SocketIOService.get_user_room(user_id)
|
||||
socketio.emit(event, data, room=room)
|
||||
logger.debug(f"Emitted {event} to user {user_id} in room {room}")
|
||||
|
||||
@staticmethod
|
||||
def emit_credits_changed(user_id: int, new_credits: int) -> None:
|
||||
"""Emit credits_changed event to a user."""
|
||||
SocketIOService.emit_to_user(
|
||||
user_id,
|
||||
"credits_changed",
|
||||
{"credits": new_credits},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_user_from_socketio() -> dict | None:
|
||||
"""Get user from SocketIO connection using cookies."""
|
||||
try:
|
||||
from flask import current_app
|
||||
from flask_jwt_extended import decode_token
|
||||
|
||||
# Check if we have the access_token cookie
|
||||
access_token = request.cookies.get("access_token_cookie")
|
||||
if not access_token:
|
||||
return None
|
||||
|
||||
# Decode the JWT token manually
|
||||
with current_app.app_context():
|
||||
try:
|
||||
decoded_token = decode_token(access_token)
|
||||
current_user_id = decoded_token["sub"]
|
||||
if not current_user_id:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Query database for user data
|
||||
from app.models.user import User
|
||||
user = User.query.get(int(current_user_id))
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"credits": user.credits,
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@socketio.on("connect")
|
||||
def handle_connect(auth=None) -> bool:
|
||||
"""Handle client connection."""
|
||||
try:
|
||||
logger.info(f"SocketIO connection from {request.remote_addr}")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error handling SocketIO connection")
|
||||
disconnect()
|
||||
return False
|
||||
|
||||
|
||||
@socketio.on("authenticate")
|
||||
def handle_authenticate(data):
|
||||
"""Handle authentication after connection."""
|
||||
try:
|
||||
user = SocketIOService.get_user_from_socketio()
|
||||
if not user:
|
||||
emit("auth_error", {"error": "Authentication failed"})
|
||||
disconnect()
|
||||
return
|
||||
|
||||
user_id = int(user["id"])
|
||||
user_room = SocketIOService.get_user_room(user_id)
|
||||
|
||||
# Join user-specific room
|
||||
join_room(user_room)
|
||||
|
||||
logger.info(f"User {user_id} authenticated and joined room {user_room}")
|
||||
|
||||
# Send current credits on authentication
|
||||
emit("auth_success", {"user": user})
|
||||
emit("credits_changed", {"credits": user["credits"]})
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error handling SocketIO authentication")
|
||||
emit("auth_error", {"error": "Authentication failed"})
|
||||
disconnect()
|
||||
|
||||
|
||||
@socketio.on("disconnect")
|
||||
def handle_disconnect() -> None:
|
||||
"""Handle client disconnection."""
|
||||
try:
|
||||
user = SocketIOService.get_user_from_socketio()
|
||||
if user:
|
||||
user_id = int(user["id"])
|
||||
user_room = SocketIOService.get_user_room(user_id)
|
||||
leave_room(user_room)
|
||||
logger.info(f"User {user_id} disconnected from SocketIO")
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error handling SocketIO disconnection")
|
||||
|
||||
|
||||
# Export the service instance
|
||||
socketio_service = SocketIOService()
|
||||
@@ -20,7 +20,7 @@ class VLCService:
|
||||
self.processes: Dict[str, subprocess.Popen] = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def play_sound(self, sound_id: int, user_id: int | None = None, ip_address: str | None = None, user_agent: str | None = None) -> bool:
|
||||
def play_sound(self, sound_id: int, user_id: int | None = None) -> bool:
|
||||
"""Play a sound by ID using VLC subprocess."""
|
||||
try:
|
||||
# Get sound from database
|
||||
@@ -86,8 +86,6 @@ class VLCService:
|
||||
SoundPlayed.create_play_record(
|
||||
user_id=user_id,
|
||||
sound_id=sound_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
commit=True,
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
6
main.py
6
main.py
@@ -1,15 +1,15 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app import create_app
|
||||
from app import create_app, socketio
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the Flask application."""
|
||||
"""Run the Flask application with SocketIO."""
|
||||
app = create_app()
|
||||
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||
socketio.run(app, debug=True, host="0.0.0.0", port=5000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies = [
|
||||
"flask-cors==6.0.1",
|
||||
"flask-jwt-extended==4.7.1",
|
||||
"flask-migrate==4.1.0",
|
||||
"flask-socketio==5.5.1",
|
||||
"flask-sqlalchemy==3.1.1",
|
||||
"pydub==0.25.1",
|
||||
"python-dotenv==1.1.1",
|
||||
@@ -21,7 +22,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["black==25.1.0", "pytest==8.4.1", "ruff==0.12.1"]
|
||||
dev = ["black==25.1.0", "pytest==8.4.1", "ruff==0.12.2"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
@@ -29,7 +30,4 @@ line-length = 80
|
||||
[tool.ruff]
|
||||
line-length = 80
|
||||
lint.select = ["ALL"]
|
||||
lint.ignore = [
|
||||
"D100", # Missing docstring in public module
|
||||
"D104", # Missing docstring in public package
|
||||
]
|
||||
lint.ignore = ["D100", "D104"]
|
||||
|
||||
122
uv.lock
generated
122
uv.lock
generated
@@ -40,6 +40,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bidict"
|
||||
version = "0.23.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "25.1.0"
|
||||
@@ -276,6 +285,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-socketio"
|
||||
version = "5.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flask" },
|
||||
{ name = "python-socketio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/1f/54d3de4982df695682af99c65d4b89f8a46fe6739780c5a68690195835a0/flask_socketio-5.5.1.tar.gz", hash = "sha256:d946c944a1074ccad8e99485a6f5c79bc5789e3ea4df0bb9c864939586c51ec4", size = 37401 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/38/1b75b3ba3452860211ec87710f9854112911a436ee4d155533e0b83b5cd9/Flask_SocketIO-5.5.1-py3-none-any.whl", hash = "sha256:35a50166db44d055f68021d6ec32cb96f1f925cd82de4504314be79139ea846f", size = 18259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-sqlalchemy"
|
||||
version = "3.1.1"
|
||||
@@ -331,6 +353,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -526,6 +557,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-engineio"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "simple-websocket" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/0b/67295279b66835f9fa7a491650efcd78b20321c127036eef62c11a31e028/python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa", size = 91677 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/fa/df59acedf7bbb937f69174d00f921a7b93aa5a5f5c17d05296c814fff6fc/python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f", size = 59536 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-socketio"
|
||||
version = "5.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bidict" },
|
||||
{ name = "python-engineio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/1a/396d50ccf06ee539fa758ce5623b59a9cb27637fc4b2dc07ed08bf495e77/python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029", size = 121125 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
@@ -543,27 +599,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -578,6 +634,7 @@ dependencies = [
|
||||
{ name = "flask-cors" },
|
||||
{ name = "flask-jwt-extended" },
|
||||
{ name = "flask-migrate" },
|
||||
{ name = "flask-socketio" },
|
||||
{ name = "flask-sqlalchemy" },
|
||||
{ name = "pydub" },
|
||||
{ name = "python-dotenv" },
|
||||
@@ -601,6 +658,7 @@ requires-dist = [
|
||||
{ name = "flask-cors", specifier = "==6.0.1" },
|
||||
{ name = "flask-jwt-extended", specifier = "==4.7.1" },
|
||||
{ name = "flask-migrate", specifier = "==4.1.0" },
|
||||
{ name = "flask-socketio", specifier = "==5.5.1" },
|
||||
{ name = "flask-sqlalchemy", specifier = "==3.1.1" },
|
||||
{ name = "pydub", specifier = "==0.25.1" },
|
||||
{ name = "python-dotenv", specifier = "==1.1.1" },
|
||||
@@ -612,7 +670,19 @@ requires-dist = [
|
||||
dev = [
|
||||
{ name = "black", specifier = "==25.1.0" },
|
||||
{ name = "pytest", specifier = "==8.4.1" },
|
||||
{ name = "ruff", specifier = "==0.12.1" },
|
||||
{ name = "ruff", specifier = "==0.12.2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-websocket"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wsproto" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -694,3 +764,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user