Refactor OAuth provider linking and unlinking logic into a dedicated service; enhance error handling and logging throughout the application; improve sound management and scanning services with better file handling and unique naming; implement centralized error and logging services for consistent API responses and application-wide logging configuration.

This commit is contained in:
JSC
2025-07-05 13:07:06 +02:00
parent 41fc197f4c
commit e2fe451e5a
17 changed files with 758 additions and 352 deletions

View File

@@ -1,10 +1,11 @@
"""Admin sound management routes."""
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 flask import Blueprint, request
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")
@@ -13,29 +14,19 @@ bp = Blueprint("admin_sounds", __name__, url_prefix="/api/admin/sounds")
@require_admin
def scan_sounds():
"""Manually trigger sound scanning."""
try:
data = request.get_json() or {}
directory = data.get("directory")
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
return ErrorHandlingService.wrap_service_call(
SoundScannerService.scan_soundboard_directory,
request.get_json().get("directory") if request.get_json() else None,
)
@bp.route("/scan/status", methods=["GET"])
@require_admin
def get_scan_status():
"""Get current scan statistics and status."""
try:
stats = SoundScannerService.get_scan_statistics()
return jsonify(stats), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
return ErrorHandlingService.wrap_service_call(
SoundScannerService.get_scan_statistics,
)
@bp.route("/normalize", methods=["POST"])
@@ -52,18 +43,21 @@ def normalize_sounds():
if sound_id:
# Normalize specific sound
result = SoundNormalizerService.normalize_sound(
sound_id, overwrite, two_pass
sound_id,
overwrite,
two_pass,
)
else:
# Normalize all sounds
result = SoundNormalizerService.normalize_all_sounds(
overwrite, limit, two_pass
overwrite,
limit,
two_pass,
)
if result["success"]:
return jsonify(result), 200
else:
return jsonify(result), 400
return jsonify(result), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -94,125 +88,26 @@ def check_ffmpeg():
@require_admin
def list_sounds():
"""Get detailed list of all sounds with normalization status."""
try:
sound_type = request.args.get("type", "SDB")
page = int(request.args.get("page", 1))
per_page = int(request.args.get("per_page", 50))
from app.services.sound_management_service import SoundManagementService
# Validate sound type
if sound_type not in ["SDB", "SAY", "STR"]:
return jsonify({"error": "Invalid sound type"}), 400
# 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
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
return ErrorHandlingService.wrap_service_call(
SoundManagementService.get_sounds_with_file_status,
request.args.get("type", "SDB"),
int(request.args.get("page", 1)),
int(request.args.get("per_page", 50)),
)
@bp.route("/<int:sound_id>", methods=["DELETE"])
@require_admin
def delete_sound(sound_id: int):
"""Delete a sound and its files."""
try:
sound = Sound.query.get(sound_id)
if not sound:
return jsonify({"error": "Sound not found"}), 404
from app.services.sound_management_service import SoundManagementService
if not sound.is_deletable:
return jsonify({"error": "Sound is not deletable"}), 403
# 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
return ErrorHandlingService.wrap_service_call(
SoundManagementService.delete_sound_with_files,
sound_id,
)
@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):
"""Normalize a specific sound."""
try:
from app.services.sound_management_service import SoundManagementService
data = request.get_json() or {}
overwrite = data.get("overwrite", False)
two_pass = data.get("two_pass", True)
result = SoundNormalizerService.normalize_sound(
sound_id, overwrite, two_pass
result = SoundManagementService.normalize_sound(
sound_id,
overwrite,
two_pass,
)
if result["success"]:
return jsonify(result), 200
else:
return jsonify(result), 400
return jsonify(result), 400
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -140,63 +140,27 @@ def link_provider(provider):
def link_callback(provider):
"""Handle OAuth callback for linking new provider."""
try:
from app.services.oauth_linking_service import OAuthLinkingService
current_user_id = get_jwt_identity()
if not current_user_id:
return {"error": "User not authenticated"}, 401
# Get current user from database
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(
result = OAuthLinkingService.link_provider_to_user(
provider,
provider_data["id"],
current_user_id,
)
return result
if existing_provider and existing_provider.user_id != user.id:
return {
"error": "This provider account is already linked to another user",
}, 409
# 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"}
except ValueError as e:
error_str = str(e)
if "not found" in error_str:
return {"error": error_str}, 404
if "not configured" in error_str:
return {"error": error_str}, 400
if "already linked" in error_str:
return {"error": error_str}, 409
return {"error": error_str}, 400
except Exception as e:
return {"error": str(e)}, 400
@@ -206,33 +170,27 @@ def link_callback(provider):
def unlink_provider(provider):
"""Unlink an OAuth provider from current user account."""
try:
from app.services.oauth_linking_service import OAuthLinkingService
current_user_id = get_jwt_identity()
if not current_user_id:
return {"error": "User not authenticated"}, 401
from app.database import db
from app.models.user import User
user = User.query.get(current_user_id)
if not user:
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"}
result = OAuthLinkingService.unlink_provider_from_user(
provider,
current_user_id,
)
return result
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:
return {"error": str(e)}, 400

View File

@@ -37,7 +37,7 @@ def get_sounds():
"sounds": sounds_data,
"total": len(sounds_data),
"type": sound_type,
}
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -60,11 +60,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
@@ -89,7 +88,7 @@ def stop_all_sounds():
{
"message": f"Force stopped {stopped_count} sounds",
"forced": True,
}
},
)
return jsonify({"message": "All sounds stopped"})
@@ -107,7 +106,7 @@ def force_stop_all_sounds():
{
"message": f"Force stopped {stopped_count} sound instances",
"stopped_count": stopped_count,
}
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -129,7 +128,7 @@ def get_status():
"id": process_id,
"pid": process.pid,
"running": process.poll() is None,
}
},
)
return jsonify(
@@ -137,7 +136,7 @@ def get_status():
"playing_count": playing_count,
"is_playing": playing_count > 0,
"processes": processes,
}
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -153,7 +152,8 @@ def get_play_history():
offset = (page - 1) * per_page
recent_plays = SoundPlayed.get_recent_plays(
limit=per_page, offset=offset
limit=per_page,
offset=offset,
)
return jsonify(
@@ -161,7 +161,7 @@ def get_play_history():
"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
@@ -182,7 +182,9 @@ def get_my_play_history():
offset = (page - 1) * per_page
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(
@@ -191,7 +193,7 @@ def get_my_play_history():
"page": page,
"per_page": per_page,
"user_id": user_id,
}
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -230,7 +232,7 @@ def get_popular_sounds():
"popular_sounds": popular_sounds,
"limit": limit,
"days": days,
}
},
)
except Exception as e:
return jsonify({"error": str(e)}), 500