Compare commits
2 Commits
41fc197f4c
...
f68d046653
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f68d046653 | ||
|
|
e2fe451e5a |
@@ -37,7 +37,9 @@ def create_app():
|
|||||||
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
|
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
|
||||||
app.config["JWT_COOKIE_SECURE"] = False # Set to True in production
|
app.config["JWT_COOKIE_SECURE"] = False # Set to True in production
|
||||||
app.config["JWT_COOKIE_CSRF_PROTECT"] = False
|
app.config["JWT_COOKIE_CSRF_PROTECT"] = False
|
||||||
app.config["JWT_ACCESS_COOKIE_PATH"] = "/" # Allow access to all paths including SocketIO
|
app.config["JWT_ACCESS_COOKIE_PATH"] = (
|
||||||
|
"/" # Allow access to all paths including SocketIO
|
||||||
|
)
|
||||||
app.config["JWT_REFRESH_COOKIE_PATH"] = "/api/auth/refresh"
|
app.config["JWT_REFRESH_COOKIE_PATH"] = "/api/auth/refresh"
|
||||||
|
|
||||||
# Initialize CORS
|
# Initialize CORS
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ def init_db(app):
|
|||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
# Import models here to ensure they are registered with SQLAlchemy
|
# Import models here to ensure they are registered with SQLAlchemy
|
||||||
from app.models import user, user_oauth, sound_played # noqa: F401
|
from app.models import sound_played, user, user_oauth # noqa: F401
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class SoundPlayed(db.Model):
|
|||||||
if result.last_played
|
if result.last_played
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return popular_sounds
|
return popular_sounds
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""Admin sound management routes."""
|
"""Admin sound management routes."""
|
||||||
|
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, jsonify, request
|
||||||
from app.models.sound import Sound
|
|
||||||
from app.services.sound_scanner_service import SoundScannerService
|
|
||||||
from app.services.sound_normalizer_service import SoundNormalizerService
|
|
||||||
from app.services.decorators import require_admin
|
from app.services.decorators import require_admin
|
||||||
|
from app.services.error_handling_service import ErrorHandlingService
|
||||||
|
from app.services.sound_normalizer_service import SoundNormalizerService
|
||||||
|
from app.services.sound_scanner_service import SoundScannerService
|
||||||
|
|
||||||
bp = Blueprint("admin_sounds", __name__, url_prefix="/api/admin/sounds")
|
bp = Blueprint("admin_sounds", __name__, url_prefix="/api/admin/sounds")
|
||||||
|
|
||||||
@@ -13,29 +14,19 @@ bp = Blueprint("admin_sounds", __name__, url_prefix="/api/admin/sounds")
|
|||||||
@require_admin
|
@require_admin
|
||||||
def scan_sounds():
|
def scan_sounds():
|
||||||
"""Manually trigger sound scanning."""
|
"""Manually trigger sound scanning."""
|
||||||
try:
|
return ErrorHandlingService.wrap_service_call(
|
||||||
data = request.get_json() or {}
|
SoundScannerService.scan_soundboard_directory,
|
||||||
directory = data.get("directory")
|
request.get_json().get("directory") if request.get_json() else None,
|
||||||
|
)
|
||||||
result = SoundScannerService.scan_soundboard_directory(directory)
|
|
||||||
|
|
||||||
if result["success"]:
|
|
||||||
return jsonify(result), 200
|
|
||||||
else:
|
|
||||||
return jsonify(result), 400
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/scan/status", methods=["GET"])
|
@bp.route("/scan/status", methods=["GET"])
|
||||||
@require_admin
|
@require_admin
|
||||||
def get_scan_status():
|
def get_scan_status():
|
||||||
"""Get current scan statistics and status."""
|
"""Get current scan statistics and status."""
|
||||||
try:
|
return ErrorHandlingService.wrap_service_call(
|
||||||
stats = SoundScannerService.get_scan_statistics()
|
SoundScannerService.get_scan_statistics,
|
||||||
return jsonify(stats), 200
|
)
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/normalize", methods=["POST"])
|
@bp.route("/normalize", methods=["POST"])
|
||||||
@@ -52,18 +43,21 @@ def normalize_sounds():
|
|||||||
if sound_id:
|
if sound_id:
|
||||||
# Normalize specific sound
|
# Normalize specific sound
|
||||||
result = SoundNormalizerService.normalize_sound(
|
result = SoundNormalizerService.normalize_sound(
|
||||||
sound_id, overwrite, two_pass
|
sound_id,
|
||||||
|
overwrite,
|
||||||
|
two_pass,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Normalize all sounds
|
# Normalize all sounds
|
||||||
result = SoundNormalizerService.normalize_all_sounds(
|
result = SoundNormalizerService.normalize_all_sounds(
|
||||||
overwrite, limit, two_pass
|
overwrite,
|
||||||
|
limit,
|
||||||
|
two_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
else:
|
return jsonify(result), 400
|
||||||
return jsonify(result), 400
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@@ -94,125 +88,26 @@ def check_ffmpeg():
|
|||||||
@require_admin
|
@require_admin
|
||||||
def list_sounds():
|
def list_sounds():
|
||||||
"""Get detailed list of all sounds with normalization status."""
|
"""Get detailed list of all sounds with normalization status."""
|
||||||
try:
|
from app.services.sound_management_service import SoundManagementService
|
||||||
sound_type = request.args.get("type", "SDB")
|
|
||||||
page = int(request.args.get("page", 1))
|
|
||||||
per_page = int(request.args.get("per_page", 50))
|
|
||||||
|
|
||||||
# Validate sound type
|
return ErrorHandlingService.wrap_service_call(
|
||||||
if sound_type not in ["SDB", "SAY", "STR"]:
|
SoundManagementService.get_sounds_with_file_status,
|
||||||
return jsonify({"error": "Invalid sound type"}), 400
|
request.args.get("type", "SDB"),
|
||||||
|
int(request.args.get("page", 1)),
|
||||||
# Get paginated results
|
int(request.args.get("per_page", 50)),
|
||||||
sounds_query = Sound.query.filter_by(type=sound_type)
|
)
|
||||||
total = sounds_query.count()
|
|
||||||
|
|
||||||
sounds = (
|
|
||||||
sounds_query.offset((page - 1) * per_page).limit(per_page).all()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert to detailed dict format
|
|
||||||
sounds_data = []
|
|
||||||
for sound in sounds:
|
|
||||||
sound_dict = sound.to_dict()
|
|
||||||
# Add file existence status
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
original_path = os.path.join(
|
|
||||||
"sounds", sound.type.lower(), sound.filename
|
|
||||||
)
|
|
||||||
sound_dict["original_exists"] = os.path.exists(original_path)
|
|
||||||
|
|
||||||
if sound.is_normalized and sound.normalized_filename:
|
|
||||||
normalized_path = os.path.join(
|
|
||||||
"sounds",
|
|
||||||
"normalized",
|
|
||||||
sound.type.lower(),
|
|
||||||
sound.normalized_filename,
|
|
||||||
)
|
|
||||||
sound_dict["normalized_exists"] = os.path.exists(
|
|
||||||
normalized_path
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sound_dict["normalized_exists"] = False
|
|
||||||
|
|
||||||
sounds_data.append(sound_dict)
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"sounds": sounds_data,
|
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
|
||||||
"per_page": per_page,
|
|
||||||
"total": total,
|
|
||||||
"pages": (total + per_page - 1) // per_page,
|
|
||||||
},
|
|
||||||
"type": sound_type,
|
|
||||||
}
|
|
||||||
), 200
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:sound_id>", methods=["DELETE"])
|
@bp.route("/<int:sound_id>", methods=["DELETE"])
|
||||||
@require_admin
|
@require_admin
|
||||||
def delete_sound(sound_id: int):
|
def delete_sound(sound_id: int):
|
||||||
"""Delete a sound and its files."""
|
"""Delete a sound and its files."""
|
||||||
try:
|
from app.services.sound_management_service import SoundManagementService
|
||||||
sound = Sound.query.get(sound_id)
|
|
||||||
if not sound:
|
|
||||||
return jsonify({"error": "Sound not found"}), 404
|
|
||||||
|
|
||||||
if not sound.is_deletable:
|
return ErrorHandlingService.wrap_service_call(
|
||||||
return jsonify({"error": "Sound is not deletable"}), 403
|
SoundManagementService.delete_sound_with_files,
|
||||||
|
sound_id,
|
||||||
# Delete normalized file if exists
|
)
|
||||||
if sound.is_normalized and sound.normalized_filename:
|
|
||||||
import os
|
|
||||||
|
|
||||||
normalized_path = os.path.join(
|
|
||||||
"sounds",
|
|
||||||
"normalized",
|
|
||||||
sound.type.lower(),
|
|
||||||
sound.normalized_filename,
|
|
||||||
)
|
|
||||||
if os.path.exists(normalized_path):
|
|
||||||
try:
|
|
||||||
os.remove(normalized_path)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify(
|
|
||||||
{"error": f"Failed to delete normalized file: {e}"}
|
|
||||||
), 500
|
|
||||||
|
|
||||||
# Delete original file
|
|
||||||
import os
|
|
||||||
|
|
||||||
original_path = os.path.join(
|
|
||||||
"sounds", sound.type.lower(), sound.filename
|
|
||||||
)
|
|
||||||
if os.path.exists(original_path):
|
|
||||||
try:
|
|
||||||
os.remove(original_path)
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify(
|
|
||||||
{"error": f"Failed to delete original file: {e}"}
|
|
||||||
), 500
|
|
||||||
|
|
||||||
# Delete database record
|
|
||||||
from app.database import db
|
|
||||||
|
|
||||||
db.session.delete(sound)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"message": f"Sound '{sound.name}' deleted successfully",
|
|
||||||
"sound_id": sound_id,
|
|
||||||
}
|
|
||||||
), 200
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:sound_id>/normalize", methods=["POST"])
|
@bp.route("/<int:sound_id>/normalize", methods=["POST"])
|
||||||
@@ -220,17 +115,20 @@ def delete_sound(sound_id: int):
|
|||||||
def normalize_single_sound(sound_id: int):
|
def normalize_single_sound(sound_id: int):
|
||||||
"""Normalize a specific sound."""
|
"""Normalize a specific sound."""
|
||||||
try:
|
try:
|
||||||
|
from app.services.sound_management_service import SoundManagementService
|
||||||
|
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
overwrite = data.get("overwrite", False)
|
overwrite = data.get("overwrite", False)
|
||||||
two_pass = data.get("two_pass", True)
|
two_pass = data.get("two_pass", True)
|
||||||
|
|
||||||
result = SoundNormalizerService.normalize_sound(
|
result = SoundManagementService.normalize_sound(
|
||||||
sound_id, overwrite, two_pass
|
sound_id,
|
||||||
|
overwrite,
|
||||||
|
two_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
else:
|
return jsonify(result), 400
|
||||||
return jsonify(result), 400
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -140,63 +140,27 @@ def link_provider(provider):
|
|||||||
def link_callback(provider):
|
def link_callback(provider):
|
||||||
"""Handle OAuth callback for linking new provider."""
|
"""Handle OAuth callback for linking new provider."""
|
||||||
try:
|
try:
|
||||||
|
from app.services.oauth_linking_service import OAuthLinkingService
|
||||||
|
|
||||||
current_user_id = get_jwt_identity()
|
current_user_id = get_jwt_identity()
|
||||||
if not current_user_id:
|
if not current_user_id:
|
||||||
return {"error": "User not authenticated"}, 401
|
return {"error": "User not authenticated"}, 401
|
||||||
|
|
||||||
# Get current user from database
|
result = OAuthLinkingService.link_provider_to_user(
|
||||||
from app.models.user import User
|
|
||||||
|
|
||||||
user = User.query.get(current_user_id)
|
|
||||||
if not user:
|
|
||||||
return {"error": "User not found"}, 404
|
|
||||||
|
|
||||||
# Process OAuth callback but link to existing user
|
|
||||||
from authlib.integrations.flask_client import OAuth
|
|
||||||
|
|
||||||
from app.services.oauth_providers.registry import OAuthProviderRegistry
|
|
||||||
|
|
||||||
oauth = OAuth()
|
|
||||||
registry = OAuthProviderRegistry(oauth)
|
|
||||||
oauth_provider = registry.get_provider(provider)
|
|
||||||
|
|
||||||
if not oauth_provider:
|
|
||||||
return {"error": f"OAuth provider '{provider}' not configured"}, 400
|
|
||||||
|
|
||||||
token = oauth_provider.exchange_code_for_token(None, None)
|
|
||||||
raw_user_info = oauth_provider.get_user_info(token)
|
|
||||||
provider_data = oauth_provider.normalize_user_data(raw_user_info)
|
|
||||||
|
|
||||||
if not provider_data.get("id"):
|
|
||||||
return {
|
|
||||||
"error": "Failed to get user information from provider",
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
# Check if this provider is already linked to another user
|
|
||||||
from app.models.user_oauth import UserOAuth
|
|
||||||
|
|
||||||
existing_provider = UserOAuth.find_by_provider_and_id(
|
|
||||||
provider,
|
provider,
|
||||||
provider_data["id"],
|
current_user_id,
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
if existing_provider and existing_provider.user_id != user.id:
|
except ValueError as e:
|
||||||
return {
|
error_str = str(e)
|
||||||
"error": "This provider account is already linked to another user",
|
if "not found" in error_str:
|
||||||
}, 409
|
return {"error": error_str}, 404
|
||||||
|
if "not configured" in error_str:
|
||||||
# Link the provider to current user
|
return {"error": error_str}, 400
|
||||||
UserOAuth.create_or_update(
|
if "already linked" in error_str:
|
||||||
user_id=user.id,
|
return {"error": error_str}, 409
|
||||||
provider=provider,
|
return {"error": error_str}, 400
|
||||||
provider_id=provider_data["id"],
|
|
||||||
email=provider_data["email"],
|
|
||||||
name=provider_data["name"],
|
|
||||||
picture=provider_data.get("picture"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": f"{provider.title()} account linked successfully"}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}, 400
|
return {"error": str(e)}, 400
|
||||||
|
|
||||||
@@ -206,33 +170,27 @@ def link_callback(provider):
|
|||||||
def unlink_provider(provider):
|
def unlink_provider(provider):
|
||||||
"""Unlink an OAuth provider from current user account."""
|
"""Unlink an OAuth provider from current user account."""
|
||||||
try:
|
try:
|
||||||
|
from app.services.oauth_linking_service import OAuthLinkingService
|
||||||
|
|
||||||
current_user_id = get_jwt_identity()
|
current_user_id = get_jwt_identity()
|
||||||
if not current_user_id:
|
if not current_user_id:
|
||||||
return {"error": "User not authenticated"}, 401
|
return {"error": "User not authenticated"}, 401
|
||||||
|
|
||||||
from app.database import db
|
result = OAuthLinkingService.unlink_provider_from_user(
|
||||||
from app.models.user import User
|
provider,
|
||||||
|
current_user_id,
|
||||||
user = User.query.get(current_user_id)
|
)
|
||||||
if not user:
|
return result
|
||||||
return {"error": "User not found"}, 404
|
|
||||||
|
|
||||||
# Check if user has more than one provider (prevent locking out)
|
|
||||||
if len(user.oauth_providers) <= 1:
|
|
||||||
return {"error": "Cannot unlink last authentication provider"}, 400
|
|
||||||
|
|
||||||
# Find and remove the provider
|
|
||||||
oauth_provider = user.get_provider(provider)
|
|
||||||
if not oauth_provider:
|
|
||||||
return {
|
|
||||||
"error": f"Provider '{provider}' not linked to this account",
|
|
||||||
}, 404
|
|
||||||
|
|
||||||
db.session.delete(oauth_provider)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return {"message": f"{provider.title()} account unlinked successfully"}
|
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
error_str = str(e)
|
||||||
|
if "not found" in error_str:
|
||||||
|
return {"error": error_str}, 404
|
||||||
|
if "Cannot unlink" in error_str:
|
||||||
|
return {"error": error_str}, 400
|
||||||
|
if "not linked" in error_str:
|
||||||
|
return {"error": error_str}, 404
|
||||||
|
return {"error": error_str}, 400
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}, 400
|
return {"error": str(e)}, 400
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def get_sounds():
|
|||||||
"sounds": sounds_data,
|
"sounds": sounds_data,
|
||||||
"total": len(sounds_data),
|
"total": len(sounds_data),
|
||||||
"type": sound_type,
|
"type": sound_type,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -60,11 +60,10 @@ def play_sound(sound_id: int):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
return jsonify({"message": "Sound playing", "sound_id": sound_id})
|
return jsonify({"message": "Sound playing", "sound_id": sound_id})
|
||||||
else:
|
return (
|
||||||
return (
|
jsonify({"error": "Sound not found or cannot be played"}),
|
||||||
jsonify({"error": "Sound not found or cannot be played"}),
|
404,
|
||||||
404,
|
)
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ def stop_all_sounds():
|
|||||||
{
|
{
|
||||||
"message": f"Force stopped {stopped_count} sounds",
|
"message": f"Force stopped {stopped_count} sounds",
|
||||||
"forced": True,
|
"forced": True,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify({"message": "All sounds stopped"})
|
return jsonify({"message": "All sounds stopped"})
|
||||||
@@ -107,7 +106,7 @@ def force_stop_all_sounds():
|
|||||||
{
|
{
|
||||||
"message": f"Force stopped {stopped_count} sound instances",
|
"message": f"Force stopped {stopped_count} sound instances",
|
||||||
"stopped_count": stopped_count,
|
"stopped_count": stopped_count,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -129,7 +128,7 @@ def get_status():
|
|||||||
"id": process_id,
|
"id": process_id,
|
||||||
"pid": process.pid,
|
"pid": process.pid,
|
||||||
"running": process.poll() is None,
|
"running": process.poll() is None,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -137,7 +136,7 @@ def get_status():
|
|||||||
"playing_count": playing_count,
|
"playing_count": playing_count,
|
||||||
"is_playing": playing_count > 0,
|
"is_playing": playing_count > 0,
|
||||||
"processes": processes,
|
"processes": processes,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -153,7 +152,8 @@ def get_play_history():
|
|||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
|
|
||||||
recent_plays = SoundPlayed.get_recent_plays(
|
recent_plays = SoundPlayed.get_recent_plays(
|
||||||
limit=per_page, offset=offset
|
limit=per_page,
|
||||||
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -161,7 +161,7 @@ def get_play_history():
|
|||||||
"plays": [play.to_dict() for play in recent_plays],
|
"plays": [play.to_dict() for play in recent_plays],
|
||||||
"page": page,
|
"page": page,
|
||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -182,7 +182,9 @@ def get_my_play_history():
|
|||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
|
|
||||||
user_plays = SoundPlayed.get_user_plays(
|
user_plays = SoundPlayed.get_user_plays(
|
||||||
user_id=user_id, limit=per_page, offset=offset
|
user_id=user_id,
|
||||||
|
limit=per_page,
|
||||||
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -191,7 +193,7 @@ def get_my_play_history():
|
|||||||
"page": page,
|
"page": page,
|
||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -230,7 +232,7 @@ def get_popular_sounds():
|
|||||||
"popular_sounds": popular_sounds,
|
"popular_sounds": popular_sounds,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"days": days,
|
"days": days,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class CreditService:
|
|||||||
for user in users:
|
for user in users:
|
||||||
if not user.plan:
|
if not user.plan:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"User {user.email} has no plan assigned, skipping"
|
f"User {user.email} has no plan assigned, skipping",
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -57,7 +57,8 @@ class CreditService:
|
|||||||
|
|
||||||
# Add daily credits but don't exceed maximum
|
# Add daily credits but don't exceed maximum
|
||||||
new_credits = min(
|
new_credits = min(
|
||||||
current_credits + plan_daily_credits, max_credits
|
current_credits + plan_daily_credits,
|
||||||
|
max_credits,
|
||||||
)
|
)
|
||||||
credits_added = new_credits - current_credits
|
credits_added = new_credits - current_credits
|
||||||
|
|
||||||
|
|||||||
@@ -188,10 +188,12 @@ def require_credits(credits_needed: int):
|
|||||||
# Emit credits changed event via SocketIO
|
# Emit credits changed event via SocketIO
|
||||||
try:
|
try:
|
||||||
from app.services.socketio_service import socketio_service
|
from app.services.socketio_service import socketio_service
|
||||||
|
|
||||||
socketio_service.emit_credits_changed(user.id, user.credits)
|
socketio_service.emit_credits_changed(user.id, user.credits)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Don't fail the request if SocketIO emission fails
|
# Don't fail the request if SocketIO emission fails
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.warning(f"Failed to emit credits_changed event: {e}")
|
logger.warning(f"Failed to emit credits_changed event: {e}")
|
||||||
|
|
||||||
|
|||||||
133
app/services/error_handling_service.py
Normal file
133
app/services/error_handling_service.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""Centralized error handling service for consistent API responses."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorHandlingService:
|
||||||
|
"""Service for standardized error handling and responses."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_validation_error(error: ValueError) -> tuple[Any, int]:
|
||||||
|
"""Handle validation errors consistently."""
|
||||||
|
error_str = str(error)
|
||||||
|
|
||||||
|
# Map common validation errors to appropriate HTTP status codes
|
||||||
|
status_code = 400
|
||||||
|
if "not found" in error_str.lower():
|
||||||
|
status_code = 404
|
||||||
|
elif (
|
||||||
|
"not authorized" in error_str.lower()
|
||||||
|
or "permission" in error_str.lower()
|
||||||
|
):
|
||||||
|
status_code = 403
|
||||||
|
elif (
|
||||||
|
"already exists" in error_str.lower()
|
||||||
|
or "already linked" in error_str.lower()
|
||||||
|
):
|
||||||
|
status_code = 409
|
||||||
|
elif (
|
||||||
|
"not configured" in error_str.lower()
|
||||||
|
or "cannot unlink" in error_str.lower()
|
||||||
|
):
|
||||||
|
status_code = 400
|
||||||
|
elif "not deletable" in error_str.lower():
|
||||||
|
status_code = 403
|
||||||
|
|
||||||
|
return jsonify({"error": error_str}), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_generic_error(error: Exception) -> tuple[Any, int]:
|
||||||
|
"""Handle generic exceptions with 500 status."""
|
||||||
|
return jsonify({"error": str(error)}), 500
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_service_result(result: dict) -> tuple[Any, int]:
|
||||||
|
"""Handle service method results that return success/error dictionaries."""
|
||||||
|
if result.get("success"):
|
||||||
|
return jsonify(result), 200
|
||||||
|
return jsonify(result), 400
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_success_response(
|
||||||
|
message: str,
|
||||||
|
data: dict = None,
|
||||||
|
status_code: int = 200,
|
||||||
|
) -> tuple[Any, int]:
|
||||||
|
"""Create a standardized success response."""
|
||||||
|
response = {"message": message}
|
||||||
|
if data:
|
||||||
|
response.update(data)
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_error_response(
|
||||||
|
message: str,
|
||||||
|
status_code: int = 400,
|
||||||
|
details: dict = None,
|
||||||
|
) -> tuple[Any, int]:
|
||||||
|
"""Create a standardized error response."""
|
||||||
|
response = {"error": message}
|
||||||
|
if details:
|
||||||
|
response.update(details)
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_auth_error(error_type: str) -> tuple[Any, int]:
|
||||||
|
"""Handle common authentication errors."""
|
||||||
|
auth_errors = {
|
||||||
|
"user_not_authenticated": ("User not authenticated", 401),
|
||||||
|
"user_not_found": ("User not found", 404),
|
||||||
|
"invalid_credentials": ("Invalid credentials", 401),
|
||||||
|
"account_disabled": ("Account is disabled", 401),
|
||||||
|
"insufficient_credits": ("Insufficient credits", 402),
|
||||||
|
"admin_required": ("Admin privileges required", 403),
|
||||||
|
}
|
||||||
|
|
||||||
|
if error_type in auth_errors:
|
||||||
|
message, status = auth_errors[error_type]
|
||||||
|
return jsonify({"error": message}), status
|
||||||
|
|
||||||
|
return jsonify({"error": "Authentication error"}), 401
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handle_file_operation_error(
|
||||||
|
operation: str, error: Exception
|
||||||
|
) -> tuple[Any, int]:
|
||||||
|
"""Handle file operation errors consistently."""
|
||||||
|
error_message = f"Failed to {operation}: {error!s}"
|
||||||
|
|
||||||
|
# Check for specific file operation errors
|
||||||
|
if (
|
||||||
|
"not found" in str(error).lower()
|
||||||
|
or "no such file" in str(error).lower()
|
||||||
|
):
|
||||||
|
return jsonify({"error": f"File not found during {operation}"}), 404
|
||||||
|
if "permission" in str(error).lower():
|
||||||
|
return jsonify(
|
||||||
|
{"error": f"Permission denied during {operation}"}
|
||||||
|
), 403
|
||||||
|
return jsonify({"error": error_message}), 500
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def wrap_service_call(service_func, *args, **kwargs) -> tuple[Any, int]:
|
||||||
|
"""Wrap service calls with standardized error handling."""
|
||||||
|
try:
|
||||||
|
result = service_func(*args, **kwargs)
|
||||||
|
|
||||||
|
# If result is a dictionary with success/error structure
|
||||||
|
if isinstance(result, dict) and "success" in result:
|
||||||
|
return ErrorHandlingService.handle_service_result(result)
|
||||||
|
|
||||||
|
# If result is a simple dictionary (like user data)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
# For other types, assume success
|
||||||
|
return jsonify({"result": result}), 200
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return ErrorHandlingService.handle_validation_error(e)
|
||||||
|
except Exception as e:
|
||||||
|
return ErrorHandlingService.handle_generic_error(e)
|
||||||
136
app/services/logging_service.py
Normal file
136
app/services/logging_service.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""Centralized logging service for the application."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingService:
|
||||||
|
"""Service for configuring and managing application logging."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setup_logging(
|
||||||
|
level: str = "INFO",
|
||||||
|
format_string: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Setup application-wide logging configuration."""
|
||||||
|
if format_string is None:
|
||||||
|
format_string = (
|
||||||
|
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure root logger
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, level.upper()),
|
||||||
|
format=format_string,
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set specific logger levels for third-party libraries
|
||||||
|
logging.getLogger("werkzeug").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_logger(name: str) -> logging.Logger:
|
||||||
|
"""Get a logger instance for a specific module."""
|
||||||
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_operation_start(logger: logging.Logger, operation: str) -> None:
|
||||||
|
"""Log the start of an operation."""
|
||||||
|
logger.info(f"Starting {operation}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_operation_success(
|
||||||
|
logger: logging.Logger,
|
||||||
|
operation: str,
|
||||||
|
details: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log successful completion of an operation."""
|
||||||
|
message = f"Successfully completed {operation}"
|
||||||
|
if details:
|
||||||
|
message += f" - {details}"
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_operation_error(
|
||||||
|
logger: logging.Logger,
|
||||||
|
operation: str,
|
||||||
|
error: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Log an error during an operation."""
|
||||||
|
logger.error(f"Error during {operation}: {error}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_validation_error(
|
||||||
|
logger: logging.Logger,
|
||||||
|
field: str,
|
||||||
|
value: str,
|
||||||
|
reason: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log validation errors consistently."""
|
||||||
|
logger.warning(f"Validation failed for {field}='{value}': {reason}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_resource_not_found(
|
||||||
|
logger: logging.Logger,
|
||||||
|
resource_type: str,
|
||||||
|
identifier: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log when a resource is not found."""
|
||||||
|
logger.warning(f"{resource_type} not found: {identifier}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_resource_created(
|
||||||
|
logger: logging.Logger,
|
||||||
|
resource_type: str,
|
||||||
|
identifier: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log when a resource is created."""
|
||||||
|
logger.info(f"Created {resource_type}: {identifier}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_resource_updated(
|
||||||
|
logger: logging.Logger,
|
||||||
|
resource_type: str,
|
||||||
|
identifier: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log when a resource is updated."""
|
||||||
|
logger.info(f"Updated {resource_type}: {identifier}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_resource_deleted(
|
||||||
|
logger: logging.Logger,
|
||||||
|
resource_type: str,
|
||||||
|
identifier: str,
|
||||||
|
) -> None:
|
||||||
|
"""Log when a resource is deleted."""
|
||||||
|
logger.info(f"Deleted {resource_type}: {identifier}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_user_action(
|
||||||
|
logger: logging.Logger,
|
||||||
|
user_id: str,
|
||||||
|
action: str,
|
||||||
|
resource: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log user actions for auditing."""
|
||||||
|
message = f"User {user_id} performed action: {action}"
|
||||||
|
if resource:
|
||||||
|
message += f" on {resource}"
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_security_event(
|
||||||
|
logger: logging.Logger,
|
||||||
|
event_type: str,
|
||||||
|
details: str,
|
||||||
|
user_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Log security-related events."""
|
||||||
|
message = f"Security event [{event_type}]: {details}"
|
||||||
|
if user_id:
|
||||||
|
message += f" (User: {user_id})"
|
||||||
|
logger.warning(message)
|
||||||
108
app/services/oauth_linking_service.py
Normal file
108
app/services/oauth_linking_service.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""OAuth provider linking service."""
|
||||||
|
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.user_oauth import UserOAuth
|
||||||
|
from app.services.oauth_providers.registry import OAuthProviderRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthLinkingService:
|
||||||
|
"""Service for linking and unlinking OAuth providers."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def link_provider_to_user(
|
||||||
|
provider: str,
|
||||||
|
current_user_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""Link a new OAuth provider to existing user account."""
|
||||||
|
# Get current user from database
|
||||||
|
user = User.query.get(current_user_id)
|
||||||
|
if not user:
|
||||||
|
raise ValueError("User not found")
|
||||||
|
|
||||||
|
# Get OAuth provider and process callback
|
||||||
|
oauth = OAuth()
|
||||||
|
registry = OAuthProviderRegistry(oauth)
|
||||||
|
oauth_provider = registry.get_provider(provider)
|
||||||
|
|
||||||
|
if not oauth_provider:
|
||||||
|
raise ValueError(f"OAuth provider '{provider}' not configured")
|
||||||
|
|
||||||
|
# Exchange code for token and get user info
|
||||||
|
token = oauth_provider.exchange_code_for_token(None, None)
|
||||||
|
raw_user_info = oauth_provider.get_user_info(token)
|
||||||
|
provider_data = oauth_provider.normalize_user_data(raw_user_info)
|
||||||
|
|
||||||
|
if not provider_data.get("id"):
|
||||||
|
raise ValueError("Failed to get user information from provider")
|
||||||
|
|
||||||
|
# Check if this provider is already linked to another user
|
||||||
|
existing_provider = UserOAuth.find_by_provider_and_id(
|
||||||
|
provider,
|
||||||
|
provider_data["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_provider and existing_provider.user_id != user.id:
|
||||||
|
raise ValueError(
|
||||||
|
"This provider account is already linked to another user",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Link the provider to current user
|
||||||
|
UserOAuth.create_or_update(
|
||||||
|
user_id=user.id,
|
||||||
|
provider=provider,
|
||||||
|
provider_id=provider_data["id"],
|
||||||
|
email=provider_data["email"],
|
||||||
|
name=provider_data["name"],
|
||||||
|
picture=provider_data.get("picture"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": f"{provider.title()} account linked successfully"}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unlink_provider_from_user(
|
||||||
|
provider: str,
|
||||||
|
current_user_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""Unlink an OAuth provider from user account."""
|
||||||
|
from app.database import db
|
||||||
|
|
||||||
|
user = User.query.get(current_user_id)
|
||||||
|
if not user:
|
||||||
|
raise ValueError("User not found")
|
||||||
|
|
||||||
|
# Check if user has more than one provider (prevent locking out)
|
||||||
|
if len(user.oauth_providers) <= 1:
|
||||||
|
raise ValueError("Cannot unlink last authentication provider")
|
||||||
|
|
||||||
|
# Find and remove the provider
|
||||||
|
oauth_provider = user.get_provider(provider)
|
||||||
|
if not oauth_provider:
|
||||||
|
raise ValueError(
|
||||||
|
f"Provider '{provider}' not linked to this account",
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.delete(oauth_provider)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {"message": f"{provider.title()} account unlinked successfully"}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_providers(user_id: int) -> dict:
|
||||||
|
"""Get all OAuth providers linked to a user."""
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if not user:
|
||||||
|
raise ValueError("User not found")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"provider": oauth.provider,
|
||||||
|
"email": oauth.email,
|
||||||
|
"name": oauth.name,
|
||||||
|
"picture": oauth.picture,
|
||||||
|
}
|
||||||
|
for oauth in user.oauth_providers
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ class SchedulerService:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Daily credit refill failed: {result['message']}"
|
f"Daily credit refill failed: {result['message']}",
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -122,7 +122,7 @@ class SchedulerService:
|
|||||||
logger.debug("Sound scan completed: no new files found")
|
logger.debug("Sound scan completed: no new files found")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Sound scan failed: {result.get('error', 'Unknown error')}"
|
f"Sound scan failed: {result.get('error', 'Unknown error')}",
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_jwt_extended import decode_token
|
|
||||||
from flask_socketio import disconnect, emit, join_room, leave_room
|
from flask_socketio import disconnect, emit, join_room, leave_room
|
||||||
|
|
||||||
from app import socketio
|
from app import socketio
|
||||||
@@ -59,6 +58,7 @@ class SocketIOService:
|
|||||||
|
|
||||||
# Query database for user data
|
# Query database for user data
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
user = User.query.get(int(current_user_id))
|
user = User.query.get(int(current_user_id))
|
||||||
if not user or not user.is_active:
|
if not user or not user.is_active:
|
||||||
return None
|
return None
|
||||||
|
|||||||
137
app/services/sound_management_service.py
Normal file
137
app/services/sound_management_service.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""Sound management service for admin operations."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from app.database import db
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.services.sound_normalizer_service import SoundNormalizerService
|
||||||
|
|
||||||
|
|
||||||
|
class SoundManagementService:
|
||||||
|
"""Service for managing sound files and database operations."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_sounds_with_file_status(
|
||||||
|
sound_type: str = "SDB",
|
||||||
|
page: int = 1,
|
||||||
|
per_page: int = 50,
|
||||||
|
) -> dict:
|
||||||
|
"""Get paginated sounds with file existence status."""
|
||||||
|
# Validate sound type
|
||||||
|
if sound_type not in ["SDB", "SAY", "STR"]:
|
||||||
|
raise ValueError("Invalid sound type")
|
||||||
|
|
||||||
|
# Get paginated results
|
||||||
|
sounds_query = Sound.query.filter_by(type=sound_type)
|
||||||
|
total = sounds_query.count()
|
||||||
|
|
||||||
|
sounds = (
|
||||||
|
sounds_query.offset((page - 1) * per_page).limit(per_page).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to detailed dict format with file status
|
||||||
|
sounds_data = []
|
||||||
|
for sound in sounds:
|
||||||
|
sound_dict = sound.to_dict()
|
||||||
|
sound_dict.update(
|
||||||
|
SoundManagementService._get_file_status(sound),
|
||||||
|
)
|
||||||
|
sounds_data.append(sound_dict)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"sounds": sounds_data,
|
||||||
|
"pagination": {
|
||||||
|
"page": page,
|
||||||
|
"per_page": per_page,
|
||||||
|
"total": total,
|
||||||
|
"pages": (total + per_page - 1) // per_page,
|
||||||
|
},
|
||||||
|
"type": sound_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_file_status(sound: Sound) -> dict:
|
||||||
|
"""Get file existence status for a sound."""
|
||||||
|
original_path = os.path.join(
|
||||||
|
"sounds",
|
||||||
|
sound.type.lower(),
|
||||||
|
sound.filename,
|
||||||
|
)
|
||||||
|
status = {"original_exists": os.path.exists(original_path)}
|
||||||
|
|
||||||
|
if sound.is_normalized and sound.normalized_filename:
|
||||||
|
normalized_path = os.path.join(
|
||||||
|
"sounds",
|
||||||
|
"normalized",
|
||||||
|
sound.type.lower(),
|
||||||
|
sound.normalized_filename,
|
||||||
|
)
|
||||||
|
status["normalized_exists"] = os.path.exists(normalized_path)
|
||||||
|
else:
|
||||||
|
status["normalized_exists"] = False
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_sound_with_files(sound_id: int) -> dict:
|
||||||
|
"""Delete a sound and its associated files."""
|
||||||
|
sound = Sound.query.get(sound_id)
|
||||||
|
if not sound:
|
||||||
|
raise ValueError("Sound not found")
|
||||||
|
|
||||||
|
if not sound.is_deletable:
|
||||||
|
raise ValueError("Sound is not deletable")
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Delete normalized file if exists
|
||||||
|
if sound.is_normalized and sound.normalized_filename:
|
||||||
|
normalized_path = os.path.join(
|
||||||
|
"sounds",
|
||||||
|
"normalized",
|
||||||
|
sound.type.lower(),
|
||||||
|
sound.normalized_filename,
|
||||||
|
)
|
||||||
|
if os.path.exists(normalized_path):
|
||||||
|
try:
|
||||||
|
os.remove(normalized_path)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Failed to delete normalized file: {e}")
|
||||||
|
|
||||||
|
# Delete original file
|
||||||
|
original_path = os.path.join(
|
||||||
|
"sounds",
|
||||||
|
sound.type.lower(),
|
||||||
|
sound.filename,
|
||||||
|
)
|
||||||
|
if os.path.exists(original_path):
|
||||||
|
try:
|
||||||
|
os.remove(original_path)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Failed to delete original file: {e}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise Exception("; ".join(errors))
|
||||||
|
|
||||||
|
# Delete database record
|
||||||
|
sound_name = sound.name
|
||||||
|
db.session.delete(sound)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": f"Sound '{sound_name}' deleted successfully",
|
||||||
|
"sound_id": sound_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize_sound(
|
||||||
|
sound_id: int,
|
||||||
|
overwrite: bool = False,
|
||||||
|
two_pass: bool = True,
|
||||||
|
) -> dict:
|
||||||
|
"""Normalize a specific sound."""
|
||||||
|
return SoundNormalizerService.normalize_sound(
|
||||||
|
sound_id,
|
||||||
|
overwrite,
|
||||||
|
two_pass,
|
||||||
|
)
|
||||||
@@ -39,7 +39,9 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize_sound(
|
def normalize_sound(
|
||||||
sound_id: int, overwrite: bool = False, two_pass: bool = True
|
sound_id: int,
|
||||||
|
overwrite: bool = False,
|
||||||
|
two_pass: bool = True,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Normalize a specific sound file using ffmpeg loudnorm.
|
"""Normalize a specific sound file using ffmpeg loudnorm.
|
||||||
|
|
||||||
@@ -250,7 +252,8 @@ class SoundNormalizerService:
|
|||||||
logger.debug("Starting first pass (analysis)")
|
logger.debug("Starting first pass (analysis)")
|
||||||
|
|
||||||
first_pass_result = SoundNormalizerService._run_first_pass(
|
first_pass_result = SoundNormalizerService._run_first_pass(
|
||||||
source_path, params
|
source_path,
|
||||||
|
params,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not first_pass_result["success"]:
|
if not first_pass_result["success"]:
|
||||||
@@ -262,7 +265,10 @@ class SoundNormalizerService:
|
|||||||
logger.debug("Starting second pass (normalization)")
|
logger.debug("Starting second pass (normalization)")
|
||||||
|
|
||||||
second_pass_result = SoundNormalizerService._run_second_pass(
|
second_pass_result = SoundNormalizerService._run_second_pass(
|
||||||
source_path, output_path, params, measured_params
|
source_path,
|
||||||
|
output_path,
|
||||||
|
params,
|
||||||
|
measured_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not second_pass_result["success"]:
|
if not second_pass_result["success"]:
|
||||||
@@ -297,7 +303,8 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_with_ffmpeg_single_pass(
|
def _normalize_with_ffmpeg_single_pass(
|
||||||
source_path: str, output_path: str
|
source_path: str,
|
||||||
|
output_path: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Run ffmpeg loudnorm on a single file using single-pass normalization.
|
"""Run ffmpeg loudnorm on a single file using single-pass normalization.
|
||||||
|
|
||||||
@@ -374,6 +381,7 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Result with measured parameters and analysis stats
|
dict: Result with measured parameters and analysis stats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Create ffmpeg input stream
|
# Create ffmpeg input stream
|
||||||
@@ -389,7 +397,10 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
# Output to null device for analysis
|
# Output to null device for analysis
|
||||||
output_stream = ffmpeg.output(
|
output_stream = ffmpeg.output(
|
||||||
input_stream, "/dev/null", af=loudnorm_filter, f="null"
|
input_stream,
|
||||||
|
"/dev/null",
|
||||||
|
af=loudnorm_filter,
|
||||||
|
f="null",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run the first pass
|
# Run the first pass
|
||||||
@@ -403,7 +414,7 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
# Parse measured parameters from JSON output
|
# Parse measured parameters from JSON output
|
||||||
measured_params = SoundNormalizerService._parse_measured_params(
|
measured_params = SoundNormalizerService._parse_measured_params(
|
||||||
stderr_text
|
stderr_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not measured_params:
|
if not measured_params:
|
||||||
@@ -446,6 +457,7 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Result with normalization stats
|
dict: Result with normalization stats
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Create ffmpeg input stream
|
# Create ffmpeg input stream
|
||||||
@@ -506,11 +518,14 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Parsed measured parameters, empty if parsing fails
|
dict: Parsed measured parameters, empty if parsing fails
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Find JSON block in stderr output
|
# Find JSON block in stderr output
|
||||||
json_match = re.search(
|
json_match = re.search(
|
||||||
r'\{[^}]*"input_i"[^}]*\}', stderr_output, re.DOTALL
|
r'\{[^}]*"input_i"[^}]*\}',
|
||||||
|
stderr_output,
|
||||||
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
if not json_match:
|
if not json_match:
|
||||||
logger.warning("No JSON block found in first pass output")
|
logger.warning("No JSON block found in first pass output")
|
||||||
|
|||||||
@@ -140,76 +140,91 @@ class SoundScannerService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _process_audio_file(file_path: str, base_dir: str) -> dict:
|
def _process_audio_file(file_path: str, base_dir: str) -> dict:
|
||||||
"""Process a single audio file and add it to database if new.
|
"""Process a single audio file and add it to database if new."""
|
||||||
|
|
||||||
Args:
|
|
||||||
file_path: Full path to the audio file
|
|
||||||
base_dir: Base directory for relative path calculation
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Processing result with added flag and reason
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Calculate file hash for deduplication
|
|
||||||
file_hash = SoundScannerService._calculate_file_hash(file_path)
|
file_hash = SoundScannerService._calculate_file_hash(file_path)
|
||||||
|
|
||||||
# Get file metadata
|
|
||||||
metadata = SoundScannerService._extract_audio_metadata(file_path)
|
metadata = SoundScannerService._extract_audio_metadata(file_path)
|
||||||
|
|
||||||
# Calculate relative filename from base directory
|
|
||||||
relative_path = Path(file_path).relative_to(Path(base_dir))
|
relative_path = Path(file_path).relative_to(Path(base_dir))
|
||||||
|
|
||||||
# Check if file already exists in database by hash
|
# Check for existing file by hash (duplicate content)
|
||||||
existing_sound = Sound.find_by_hash(file_hash)
|
if existing_sound := Sound.find_by_hash(file_hash):
|
||||||
if existing_sound:
|
return SoundScannerService._handle_duplicate_file(existing_sound)
|
||||||
return {
|
|
||||||
"added": False,
|
|
||||||
"reason": f"File already exists as '{existing_sound.name}'",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if filename already exists in database
|
# Check for existing filename (file replacement)
|
||||||
existing_filename_sound = Sound.find_by_filename(str(relative_path))
|
if existing_filename_sound := Sound.find_by_filename(
|
||||||
if existing_filename_sound:
|
str(relative_path)
|
||||||
# Remove normalized files and clear normalized info
|
):
|
||||||
SoundScannerService._clear_normalized_files(existing_filename_sound)
|
return SoundScannerService._handle_file_replacement(
|
||||||
existing_filename_sound.clear_normalized_info()
|
existing_filename_sound,
|
||||||
|
str(relative_path),
|
||||||
# Update existing sound with new file information
|
metadata,
|
||||||
existing_filename_sound.update_file_info(
|
file_hash,
|
||||||
filename=str(relative_path),
|
|
||||||
duration=metadata["duration"],
|
|
||||||
size=metadata["size"],
|
|
||||||
hash_value=file_hash,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
|
||||||
"added": False,
|
|
||||||
"updated": True,
|
|
||||||
"sound_id": existing_filename_sound.id,
|
|
||||||
"reason": f"Updated existing sound '{existing_filename_sound.name}' with new file data",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate sound name from filename (without extension)
|
|
||||||
sound_name = Path(file_path).stem
|
|
||||||
|
|
||||||
# Check if name already exists and make it unique if needed
|
|
||||||
counter = 1
|
|
||||||
original_name = sound_name
|
|
||||||
while Sound.find_by_name(sound_name):
|
|
||||||
sound_name = f"{original_name}_{counter}"
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# Create new sound record
|
# Create new sound record
|
||||||
|
return SoundScannerService._create_new_sound(
|
||||||
|
file_path,
|
||||||
|
str(relative_path),
|
||||||
|
metadata,
|
||||||
|
file_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_duplicate_file(existing_sound: Sound) -> dict:
|
||||||
|
"""Handle case where file content already exists in database."""
|
||||||
|
return {
|
||||||
|
"added": False,
|
||||||
|
"reason": f"File already exists as '{existing_sound.name}'",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_file_replacement(
|
||||||
|
existing_sound: Sound,
|
||||||
|
relative_path: str,
|
||||||
|
metadata: dict,
|
||||||
|
file_hash: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Handle case where filename exists but content may be different."""
|
||||||
|
# Remove normalized files and clear normalized info
|
||||||
|
SoundScannerService._clear_normalized_files(existing_sound)
|
||||||
|
existing_sound.clear_normalized_info()
|
||||||
|
|
||||||
|
# Update existing sound with new file information
|
||||||
|
existing_sound.update_file_info(
|
||||||
|
filename=relative_path,
|
||||||
|
duration=metadata["duration"],
|
||||||
|
size=metadata["size"],
|
||||||
|
hash_value=file_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"added": False,
|
||||||
|
"updated": True,
|
||||||
|
"sound_id": existing_sound.id,
|
||||||
|
"reason": f"Updated existing sound '{existing_sound.name}' with new file data",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_new_sound(
|
||||||
|
file_path: str,
|
||||||
|
relative_path: str,
|
||||||
|
metadata: dict,
|
||||||
|
file_hash: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Create a new sound record in the database."""
|
||||||
|
sound_name = SoundScannerService._generate_unique_sound_name(
|
||||||
|
Path(file_path).stem,
|
||||||
|
)
|
||||||
|
|
||||||
sound = Sound.create_sound(
|
sound = Sound.create_sound(
|
||||||
sound_type="SDB", # Soundboard type
|
sound_type="SDB",
|
||||||
name=sound_name,
|
name=sound_name,
|
||||||
filename=str(relative_path),
|
filename=relative_path,
|
||||||
duration=metadata["duration"],
|
duration=metadata["duration"],
|
||||||
size=metadata["size"],
|
size=metadata["size"],
|
||||||
hash_value=file_hash,
|
hash_value=file_hash,
|
||||||
is_music=False,
|
is_music=False,
|
||||||
is_deletable=False,
|
is_deletable=False,
|
||||||
commit=False, # Don't commit individually, let scanner handle transaction
|
commit=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -218,6 +233,18 @@ class SoundScannerService:
|
|||||||
"reason": "New file added successfully",
|
"reason": "New file added successfully",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_unique_sound_name(base_name: str) -> str:
|
||||||
|
"""Generate a unique sound name by appending numbers if needed."""
|
||||||
|
sound_name = base_name
|
||||||
|
counter = 1
|
||||||
|
|
||||||
|
while Sound.find_by_name(sound_name):
|
||||||
|
sound_name = f"{base_name}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return sound_name
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _calculate_file_hash(file_path: str) -> str:
|
def _calculate_file_hash(file_path: str) -> str:
|
||||||
"""Calculate SHA256 hash of file contents."""
|
"""Calculate SHA256 hash of file contents."""
|
||||||
@@ -249,7 +276,7 @@ class SoundScannerService:
|
|||||||
logger.info(f"Removed normalized file: {normalized_path}")
|
logger.info(f"Removed normalized file: {normalized_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Could not remove normalized file {normalized_path}: {e}"
|
f"Could not remove normalized file {normalized_path}: {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"""VLC service for playing sounds using subprocess."""
|
"""VLC service for playing sounds using subprocess."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from app.database import db
|
|
||||||
from app.models.sound import Sound
|
from app.models.sound import Sound
|
||||||
from app.models.sound_played import SoundPlayed
|
from app.models.sound_played import SoundPlayed
|
||||||
|
from app.services.logging_service import LoggingService
|
||||||
|
|
||||||
|
logger = LoggingService.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VLCService:
|
class VLCService:
|
||||||
@@ -17,7 +17,7 @@ class VLCService:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize VLC service."""
|
"""Initialize VLC service."""
|
||||||
self.processes: Dict[str, subprocess.Popen] = {}
|
self.processes: dict[str, subprocess.Popen] = {}
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def play_sound(self, sound_id: int, user_id: int | None = None) -> bool:
|
def play_sound(self, sound_id: int, user_id: int | None = None) -> bool:
|
||||||
@@ -38,7 +38,9 @@ class VLCService:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sound_path = os.path.join(
|
sound_path = os.path.join(
|
||||||
"sounds", "soundboard", sound.filename
|
"sounds",
|
||||||
|
"soundboard",
|
||||||
|
sound.filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if file exists
|
# Check if file exists
|
||||||
@@ -73,8 +75,9 @@ class VLCService:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
self.processes[process_id] = process
|
self.processes[process_id] = process
|
||||||
|
|
||||||
print(
|
logger.info(
|
||||||
f"Started VLC process {process.pid} ({process_id}) for sound {sound.name}. Total processes: {len(self.processes)}"
|
f"Started VLC process {process.pid} for sound '{sound.name}'. "
|
||||||
|
f"Total active processes: {len(self.processes)}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Increment play count
|
# Increment play count
|
||||||
@@ -89,7 +92,7 @@ class VLCService:
|
|||||||
commit=True,
|
commit=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error recording play event: {e}")
|
logger.error(f"Error recording play event: {e}")
|
||||||
|
|
||||||
# Schedule cleanup after sound duration
|
# Schedule cleanup after sound duration
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
@@ -101,7 +104,9 @@ class VLCService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting VLC process for sound {sound_id}: {e}")
|
logger.error(
|
||||||
|
f"Error starting VLC process for sound {sound_id}: {e}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cleanup_after_playback(self, process_id: str, duration: int) -> None:
|
def _cleanup_after_playback(self, process_id: str, duration: int) -> None:
|
||||||
@@ -111,13 +116,13 @@ class VLCService:
|
|||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if process_id in self.processes:
|
if process_id in self.processes:
|
||||||
print(f"Cleaning up process {process_id} after playback")
|
logger.debug(f"Cleaning up process {process_id} after playback")
|
||||||
process = self.processes[process_id]
|
process = self.processes[process_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if process is still running
|
# Check if process is still running
|
||||||
if process.poll() is None:
|
if process.poll() is None:
|
||||||
print(
|
logger.debug(
|
||||||
f"Process {process.pid} still running, terminating"
|
f"Process {process.pid} still running, terminating"
|
||||||
)
|
)
|
||||||
process.terminate()
|
process.terminate()
|
||||||
@@ -125,62 +130,58 @@ class VLCService:
|
|||||||
try:
|
try:
|
||||||
process.wait(timeout=2)
|
process.wait(timeout=2)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
print(
|
logger.debug(
|
||||||
f"Process {process.pid} didn't terminate, killing"
|
f"Process {process.pid} didn't terminate, killing"
|
||||||
)
|
)
|
||||||
process.kill()
|
process.kill()
|
||||||
|
|
||||||
print(f"Successfully cleaned up process {process_id}")
|
logger.debug(
|
||||||
|
f"Successfully cleaned up process {process_id}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during cleanup of {process_id}: {e}")
|
logger.warning(f"Error during cleanup of {process_id}: {e}")
|
||||||
finally:
|
finally:
|
||||||
# Always remove from tracking
|
# Always remove from tracking
|
||||||
del self.processes[process_id]
|
del self.processes[process_id]
|
||||||
print(
|
logger.debug(
|
||||||
f"Removed process {process_id}. Remaining processes: {len(self.processes)}"
|
f"Removed process {process_id}. Remaining processes: {len(self.processes)}",
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
print(f"Process {process_id} not found during cleanup")
|
|
||||||
|
|
||||||
def stop_all(self) -> None:
|
def stop_all(self) -> None:
|
||||||
"""Stop all playing sounds by killing VLC processes."""
|
"""Stop all playing sounds by killing VLC processes."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
processes_copy = dict(self.processes)
|
processes_copy = dict(self.processes)
|
||||||
print(
|
if processes_copy:
|
||||||
f"Stopping {len(processes_copy)} VLC processes: {list(processes_copy.keys())}"
|
logger.info(f"Stopping {len(processes_copy)} VLC processes")
|
||||||
)
|
|
||||||
|
|
||||||
for process_id, process in processes_copy.items():
|
for process_id, process in processes_copy.items():
|
||||||
try:
|
try:
|
||||||
if process.poll() is None: # Process is still running
|
if process.poll() is None: # Process is still running
|
||||||
print(
|
logger.debug(f"Terminating process {process.pid}")
|
||||||
f"Terminating process {process.pid} ({process_id})"
|
|
||||||
)
|
|
||||||
process.terminate()
|
process.terminate()
|
||||||
|
|
||||||
# Give it a moment to terminate gracefully
|
# Give it a moment to terminate gracefully
|
||||||
try:
|
try:
|
||||||
process.wait(timeout=1)
|
process.wait(timeout=1)
|
||||||
print(
|
logger.debug(
|
||||||
f"Process {process.pid} terminated gracefully"
|
f"Process {process.pid} terminated gracefully"
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
print(
|
logger.debug(
|
||||||
f"Process {process.pid} didn't terminate, killing forcefully"
|
f"Process {process.pid} didn't terminate, killing forcefully"
|
||||||
)
|
)
|
||||||
process.kill()
|
process.kill()
|
||||||
process.wait() # Wait for it to be killed
|
process.wait() # Wait for it to be killed
|
||||||
else:
|
else:
|
||||||
print(
|
logger.debug(f"Process {process.pid} already finished")
|
||||||
f"Process {process.pid} ({process_id}) already finished"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error stopping process {process_id}: {e}")
|
logger.warning(f"Error stopping process {process_id}: {e}")
|
||||||
|
|
||||||
# Clear all processes
|
# Clear all processes
|
||||||
self.processes.clear()
|
self.processes.clear()
|
||||||
print(f"Cleared all processes. Remaining: {len(self.processes)}")
|
if processes_copy:
|
||||||
|
logger.info("All VLC processes stopped")
|
||||||
|
|
||||||
def get_playing_count(self) -> int:
|
def get_playing_count(self) -> int:
|
||||||
"""Get number of currently playing sounds."""
|
"""Get number of currently playing sounds."""
|
||||||
@@ -201,34 +202,20 @@ class VLCService:
|
|||||||
"""Force stop all sounds by killing VLC processes aggressively."""
|
"""Force stop all sounds by killing VLC processes aggressively."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
stopped_count = len(self.processes)
|
stopped_count = len(self.processes)
|
||||||
print(f"Force stopping {stopped_count} VLC processes")
|
if stopped_count > 0:
|
||||||
|
logger.warning(f"Force stopping {stopped_count} VLC processes")
|
||||||
# # Kill all VLC processes aggressively
|
|
||||||
# for process_id, process in list(self.processes.items()):
|
|
||||||
# try:
|
|
||||||
# if process.poll() is None: # Process is still running
|
|
||||||
# print(f"Force killing process {process.pid} ({process_id})")
|
|
||||||
# process.kill()
|
|
||||||
# process.wait() # Wait for it to be killed
|
|
||||||
# print(f"Process {process.pid} killed")
|
|
||||||
# else:
|
|
||||||
# print(f"Process {process.pid} ({process_id}) already finished")
|
|
||||||
|
|
||||||
# except Exception as e:
|
|
||||||
# print(f"Error force-stopping process {process_id}: {e}")
|
|
||||||
|
|
||||||
# Also try to kill any remaining VLC processes system-wide
|
# Also try to kill any remaining VLC processes system-wide
|
||||||
try:
|
try:
|
||||||
subprocess.run(["pkill", "-f", "vlc"], check=False)
|
subprocess.run(["pkill", "-f", "vlc"], check=False)
|
||||||
print("Killed any remaining VLC processes system-wide")
|
logger.info("Killed any remaining VLC processes system-wide")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error killing system VLC processes: {e}")
|
logger.error(f"Error killing system VLC processes: {e}")
|
||||||
|
|
||||||
# Clear all processes
|
# Clear all processes
|
||||||
self.processes.clear()
|
self.processes.clear()
|
||||||
print(
|
if stopped_count > 0:
|
||||||
f"Force stop completed. Processes remaining: {len(self.processes)}"
|
logger.info("Force stop completed")
|
||||||
)
|
|
||||||
return stopped_count
|
return stopped_count
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user