diff --git a/app/api/v1/auth.py b/app/api/v1/auth.py index 85d23f2..906bdd4 100644 --- a/app/api/v1/auth.py +++ b/app/api/v1/auth.py @@ -181,7 +181,9 @@ async def logout( user_id = int(user_id_str) user = await auth_service.get_current_user(user_id) logger.info("Found user from access token: %s", user.email) - except (HTTPException, Exception) as e: + except HTTPException as e: + logger.info("Access token validation failed: %s", str(e)) + except Exception as e: # noqa: BLE001 logger.info("Access token validation failed: %s", str(e)) # If no user found, try refresh token @@ -193,7 +195,9 @@ async def logout( user_id = int(user_id_str) user = await auth_service.get_current_user(user_id) logger.info("Found user from refresh token: %s", user.email) - except (HTTPException, Exception) as e: + except HTTPException as e: + logger.info("Refresh token validation failed: %s", str(e)) + except Exception as e: # noqa: BLE001 logger.info("Refresh token validation failed: %s", str(e)) # If we found a user, revoke their refresh token @@ -484,7 +488,6 @@ async def change_password( await auth_service.change_user_password( current_user, request.current_password, request.new_password, ) - return {"message": "Password changed successfully"} except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -496,6 +499,8 @@ async def change_password( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to change password", ) from e + else: + return {"message": "Password changed successfully"} @router.get("/user-providers") diff --git a/app/api/v1/dashboard.py b/app/api/v1/dashboard.py index db18e71..2920367 100644 --- a/app/api/v1/dashboard.py +++ b/app/api/v1/dashboard.py @@ -33,9 +33,18 @@ async def get_track_statistics( async def get_top_sounds( _current_user: Annotated[User, Depends(get_current_user)], dashboard_service: Annotated[DashboardService, Depends(get_dashboard_service)], - sound_type: Annotated[str, Query(description="Sound type filter (SDB, TTS, EXT, or 'all')")], - period: Annotated[str, Query(description="Time period (today, 1_day, 1_week, 1_month, 1_year, all_time)")] = "all_time", - limit: Annotated[int, Query(description="Number of top sounds to return", ge=1, le=100)] = 10, + sound_type: Annotated[ + str, Query(description="Sound type filter (SDB, TTS, EXT, or 'all')"), + ], + period: Annotated[ + str, + Query( + description="Time period (today, 1_day, 1_week, 1_month, 1_year, all_time)", + ), + ] = "all_time", + limit: Annotated[ + int, Query(description="Number of top sounds to return", ge=1, le=100), + ] = 10, ) -> list[dict[str, Any]]: """Get top sounds by play count for a specific period.""" return await dashboard_service.get_top_sounds( diff --git a/app/api/v1/main.py b/app/api/v1/main.py index fd28bd5..bf73114 100644 --- a/app/api/v1/main.py +++ b/app/api/v1/main.py @@ -69,7 +69,10 @@ async def elements_docs() -> HTMLResponse: - + API Documentation - elements diff --git a/app/api/v1/playlists.py b/app/api/v1/playlists.py index a129494..573f7e1 100644 --- a/app/api/v1/playlists.py +++ b/app/api/v1/playlists.py @@ -32,7 +32,7 @@ async def get_playlist_service( @router.get("/") -async def get_all_playlists( +async def get_all_playlists( # noqa: PLR0913 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], search: Annotated[ @@ -57,7 +57,7 @@ async def get_all_playlists( ] = 0, ) -> list[dict]: """Get all playlists from all users with search and sorting.""" - playlists = await playlist_service.search_and_sort_playlists( + return await playlist_service.search_and_sort_playlists( search_query=search, sort_by=sort_by, sort_order=sort_order, @@ -66,7 +66,6 @@ async def get_all_playlists( limit=limit, offset=offset, ) - return playlists @router.get("/user") diff --git a/app/api/v1/sounds.py b/app/api/v1/sounds.py index 195a4ce..a7b1f24 100644 --- a/app/api/v1/sounds.py +++ b/app/api/v1/sounds.py @@ -35,7 +35,7 @@ async def get_sound_repository( @router.get("/") -async def get_sounds( +async def get_sounds( # noqa: PLR0913 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 sound_repo: Annotated[SoundRepository, Depends(get_sound_repository)], types: Annotated[ diff --git a/app/main.py b/app/main.py index 2634a2d..2ab0c09 100644 --- a/app/main.py +++ b/app/main.py @@ -60,7 +60,10 @@ def create_app() -> FastAPI: """Create and configure the FastAPI application.""" app = FastAPI( title="Soundboard API", - description="API for the Soundboard application with authentication, sound management, and real-time features", + description=( + "API for the Soundboard application with authentication, " + "sound management, and real-time features" + ), version="1.0.0", lifespan=lifespan, # Configure docs URLs for reverse proxy setup diff --git a/app/repositories/playlist.py b/app/repositories/playlist.py index bcca57d..14dcd79 100644 --- a/app/repositories/playlist.py +++ b/app/repositories/playlist.py @@ -132,7 +132,9 @@ class PlaylistRepository(BaseRepository[Playlist]): result = await self.session.exec(statement) return list(result.all()) except Exception: - logger.exception("Failed to get playlist sound entries for playlist: %s", playlist_id) + logger.exception( + "Failed to get playlist sound entries for playlist: %s", playlist_id, + ) raise async def add_sound_to_playlist( @@ -302,19 +304,22 @@ class PlaylistRepository(BaseRepository[Playlist]): ) raise - async def search_and_sort( + async def search_and_sort( # noqa: C901, PLR0913, PLR0912, PLR0915 self, search_query: str | None = None, sort_by: PlaylistSortField | None = None, sort_order: SortOrder = SortOrder.ASC, user_id: int | None = None, - include_stats: bool = False, + include_stats: bool = False, # noqa: FBT001, FBT002 limit: int | None = None, offset: int = 0, ) -> list[dict]: """Search and sort playlists with optional statistics.""" try: - if include_stats and sort_by in (PlaylistSortField.SOUND_COUNT, PlaylistSortField.TOTAL_DURATION): + if include_stats and sort_by in ( + PlaylistSortField.SOUND_COUNT, + PlaylistSortField.TOTAL_DURATION, + ): # Use subquery for sorting by stats subquery = ( select( @@ -329,12 +334,18 @@ class PlaylistRepository(BaseRepository[Playlist]): Playlist.created_at, Playlist.updated_at, func.count(PlaylistSound.id).label("sound_count"), - func.coalesce(func.sum(Sound.duration), 0).label("total_duration"), + func.coalesce(func.sum(Sound.duration), 0).label( + "total_duration", + ), User.name.label("user_name"), ) .select_from(Playlist) .join(User, Playlist.user_id == User.id, isouter=True) - .join(PlaylistSound, Playlist.id == PlaylistSound.playlist_id, isouter=True) + .join( + PlaylistSound, + Playlist.id == PlaylistSound.playlist_id, + isouter=True, + ) .join(Sound, PlaylistSound.sound_id == Sound.id, isouter=True) .group_by(Playlist.id, User.name) ) @@ -342,7 +353,9 @@ class PlaylistRepository(BaseRepository[Playlist]): # Apply filters if search_query and search_query.strip(): search_pattern = f"%{search_query.strip().lower()}%" - subquery = subquery.where(func.lower(Playlist.name).like(search_pattern)) + subquery = subquery.where( + func.lower(Playlist.name).like(search_pattern), + ) if user_id is not None: subquery = subquery.where(Playlist.user_id == user_id) @@ -350,14 +363,20 @@ class PlaylistRepository(BaseRepository[Playlist]): # Apply sorting if sort_by == PlaylistSortField.SOUND_COUNT: if sort_order == SortOrder.DESC: - subquery = subquery.order_by(func.count(PlaylistSound.id).desc()) + subquery = subquery.order_by( + func.count(PlaylistSound.id).desc(), + ) else: subquery = subquery.order_by(func.count(PlaylistSound.id).asc()) elif sort_by == PlaylistSortField.TOTAL_DURATION: if sort_order == SortOrder.DESC: - subquery = subquery.order_by(func.coalesce(func.sum(Sound.duration), 0).desc()) + subquery = subquery.order_by( + func.coalesce(func.sum(Sound.duration), 0).desc(), + ) else: - subquery = subquery.order_by(func.coalesce(func.sum(Sound.duration), 0).asc()) + subquery = subquery.order_by( + func.coalesce(func.sum(Sound.duration), 0).asc(), + ) else: # Default sorting by name subquery = subquery.order_by(Playlist.name.asc()) @@ -377,12 +396,18 @@ class PlaylistRepository(BaseRepository[Playlist]): Playlist.created_at, Playlist.updated_at, func.count(PlaylistSound.id).label("sound_count"), - func.coalesce(func.sum(Sound.duration), 0).label("total_duration"), + func.coalesce(func.sum(Sound.duration), 0).label( + "total_duration", + ), User.name.label("user_name"), ) .select_from(Playlist) .join(User, Playlist.user_id == User.id, isouter=True) - .join(PlaylistSound, Playlist.id == PlaylistSound.playlist_id, isouter=True) + .join( + PlaylistSound, + Playlist.id == PlaylistSound.playlist_id, + isouter=True, + ) .join(Sound, PlaylistSound.sound_id == Sound.id, isouter=True) .group_by(Playlist.id, User.name) ) @@ -390,7 +415,9 @@ class PlaylistRepository(BaseRepository[Playlist]): # Apply filters if search_query and search_query.strip(): search_pattern = f"%{search_query.strip().lower()}%" - subquery = subquery.where(func.lower(Playlist.name).like(search_pattern)) + subquery = subquery.where( + func.lower(Playlist.name).like(search_pattern), + ) if user_id is not None: subquery = subquery.where(Playlist.user_id == user_id) @@ -426,9 +453,8 @@ class PlaylistRepository(BaseRepository[Playlist]): rows = result.all() # Convert to dictionary format - playlists = [] - for row in rows: - playlists.append({ + playlists = [ + { "id": row.id, "name": row.name, "description": row.description, @@ -442,12 +468,20 @@ class PlaylistRepository(BaseRepository[Playlist]): "updated_at": row.updated_at, "sound_count": row.sound_count or 0, "total_duration": row.total_duration or 0, - }) + } + for row in rows + ] - return playlists except Exception: logger.exception( - "Failed to search and sort playlists: query=%s, sort_by=%s, sort_order=%s", - search_query, sort_by, sort_order, + ( + "Failed to search and sort playlists: " + "query=%s, sort_by=%s, sort_order=%s" + ), + search_query, + sort_by, + sort_order, ) raise + else: + return playlists diff --git a/app/repositories/sound.py b/app/repositories/sound.py index 7447b9a..c2581f4 100644 --- a/app/repositories/sound.py +++ b/app/repositories/sound.py @@ -132,7 +132,7 @@ class SoundRepository(BaseRepository[Sound]): logger.exception("Failed to get sounds by types: %s", sound_types) raise - async def search_and_sort( + async def search_and_sort( # noqa: PLR0913 self, search_query: str | None = None, sound_types: list[str] | None = None, @@ -177,8 +177,14 @@ class SoundRepository(BaseRepository[Sound]): return list(result.all()) except Exception: logger.exception( - "Failed to search and sort sounds: query=%s, types=%s, sort_by=%s, sort_order=%s", - search_query, sound_types, sort_by, sort_order, + ( + "Failed to search and sort sounds: " + "query=%s, types=%s, sort_by=%s, sort_order=%s" + ), + search_query, + sound_types, + sort_by, + sort_order, ) raise @@ -189,21 +195,26 @@ class SoundRepository(BaseRepository[Sound]): func.count(Sound.id).label("count"), func.sum(Sound.play_count).label("total_plays"), func.sum(Sound.duration).label("total_duration"), - func.sum(Sound.size + func.coalesce(Sound.normalized_size, 0)).label("total_size"), + func.sum( + Sound.size + func.coalesce(Sound.normalized_size, 0), + ).label("total_size"), ).where(Sound.type == "SDB") result = await self.session.exec(statement) row = result.first() - return { - "count": row.count if row.count is not None else 0, - "total_plays": row.total_plays if row.total_plays is not None else 0, - "total_duration": row.total_duration if row.total_duration is not None else 0, - "total_size": row.total_size if row.total_size is not None else 0, - } except Exception: logger.exception("Failed to get soundboard statistics") raise + else: + return { + "count": row.count if row.count is not None else 0, + "total_plays": row.total_plays if row.total_plays is not None else 0, + "total_duration": ( + row.total_duration if row.total_duration is not None else 0 + ), + "total_size": row.total_size if row.total_size is not None else 0, + } async def get_track_statistics(self) -> dict[str, int | float]: """Get statistics for EXT type sounds.""" @@ -212,21 +223,26 @@ class SoundRepository(BaseRepository[Sound]): func.count(Sound.id).label("count"), func.sum(Sound.play_count).label("total_plays"), func.sum(Sound.duration).label("total_duration"), - func.sum(Sound.size + func.coalesce(Sound.normalized_size, 0)).label("total_size"), + func.sum( + Sound.size + func.coalesce(Sound.normalized_size, 0), + ).label("total_size"), ).where(Sound.type == "EXT") result = await self.session.exec(statement) row = result.first() - return { - "count": row.count if row.count is not None else 0, - "total_plays": row.total_plays if row.total_plays is not None else 0, - "total_duration": row.total_duration if row.total_duration is not None else 0, - "total_size": row.total_size if row.total_size is not None else 0, - } except Exception: logger.exception("Failed to get track statistics") raise + else: + return { + "count": row.count if row.count is not None else 0, + "total_plays": row.total_plays if row.total_plays is not None else 0, + "total_duration": ( + row.total_duration if row.total_duration is not None else 0 + ), + "total_size": row.total_size if row.total_size is not None else 0, + } async def get_top_sounds( self, @@ -234,7 +250,7 @@ class SoundRepository(BaseRepository[Sound]): date_filter: datetime | None = None, limit: int = 10, ) -> list[dict]: - """Get top sounds by play count for a specific type and period using SoundPlayed records.""" + """Get top sounds by play count for a specific type and period.""" try: # Join SoundPlayed with Sound and count plays within the period statement = ( diff --git a/app/schemas/auth.py b/app/schemas/auth.py index 7c603b0..60614c7 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -84,7 +84,9 @@ class ApiTokenStatusResponse(BaseModel): class ChangePasswordRequest(BaseModel): """Schema for password change request.""" - current_password: str | None = Field(None, description="Current password (required if user has existing password)") + current_password: str | None = Field( + None, description="Current password (required if user has existing password)", + ) new_password: str = Field( ..., min_length=8, diff --git a/app/services/auth.py b/app/services/auth.py index f33fa4d..c6c89c0 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -467,11 +467,13 @@ class AuthService: # If user has existing password, verify it if had_existing_password: if not current_password: - raise ValueError("Current password is required when changing existing password") + msg = "Current password is required when changing existing password" + raise ValueError(msg) if not PasswordUtils.verify_password(current_password, user.password_hash): - raise ValueError("Current password is incorrect") + msg = "Current password is incorrect" + raise ValueError(msg) else: - # User doesn't have a password (OAuth-only user), so we're setting their first password + # User doesn't have a password (OAuth-only user), setting first password logger.info("Setting first password for OAuth user: %s", user_email) # Hash new password @@ -509,6 +511,6 @@ class AuthService: updated_at=user.updated_at, ) - async def get_user_oauth_providers(self, user: User): + async def get_user_oauth_providers(self, user: User) -> list: """Get OAuth providers connected to the user.""" return await self.oauth_repo.get_by_user_id(user.id) diff --git a/app/services/credit.py b/app/services/credit.py index a0f810b..696dda3 100644 --- a/app/services/credit.py +++ b/app/services/credit.py @@ -223,11 +223,11 @@ class CreditService: user_id, ) - return transaction - except Exception: await session.rollback() raise + else: + return transaction finally: await session.close() @@ -316,11 +316,11 @@ class CreditService: user_id, ) - return transaction - except Exception: await session.rollback() raise + else: + return transaction finally: await session.close() @@ -503,11 +503,11 @@ class CreditService: user_id, ) - return transaction - except Exception: await session.rollback() raise + else: + return transaction finally: await session.close() diff --git a/app/services/dashboard.py b/app/services/dashboard.py index 29ed4f8..a034f0f 100644 --- a/app/services/dashboard.py +++ b/app/services/dashboard.py @@ -87,7 +87,7 @@ class DashboardService: ) raise - def _get_date_filter(self, period: str) -> datetime | None: + def _get_date_filter(self, period: str) -> datetime | None: # noqa: PLR0911 """Calculate the date filter based on the period.""" now = datetime.now(UTC) diff --git a/app/services/playlist.py b/app/services/playlist.py index d655ed8..338cac3 100644 --- a/app/services/playlist.py +++ b/app/services/playlist.py @@ -212,12 +212,13 @@ class PlaylistService: """Search all playlists by name.""" return await self.playlist_repo.search_by_name(query) - async def search_and_sort_playlists( + async def search_and_sort_playlists( # noqa: PLR0913 self, search_query: str | None = None, sort_by: PlaylistSortField | None = None, sort_order: SortOrder = SortOrder.ASC, user_id: int | None = None, + *, include_stats: bool = False, limit: int | None = None, offset: int = 0, @@ -269,7 +270,7 @@ class PlaylistService: current_sounds = await self.playlist_repo.get_playlist_sounds(playlist_id) position = len(current_sounds) else: - # Ensure position doesn't create gaps - if position is too high, place at end + # Ensure position doesn't create gaps - if too high, place at end current_sounds = await self.playlist_repo.get_playlist_sounds(playlist_id) max_position = len(current_sounds) position = min(position, max_position) @@ -329,7 +330,11 @@ class PlaylistService: # Create sequential positions: 0, 1, 2, 3... sound_positions = [(sound.id, index) for index, sound in enumerate(sounds)] await self.playlist_repo.reorder_playlist_sounds(playlist_id, sound_positions) - logger.debug("Reordered %s sounds in playlist %s to eliminate gaps", len(sounds), playlist_id) + logger.debug( + "Reordered %s sounds in playlist %s to eliminate gaps", + len(sounds), + playlist_id, + ) async def reorder_playlist_sounds( self, diff --git a/app/utils/audio.py b/app/utils/audio.py index b683525..9ebaac1 100644 --- a/app/utils/audio.py +++ b/app/utils/audio.py @@ -34,7 +34,7 @@ def get_audio_duration(file_path: Path) -> int: probe = ffmpeg.probe(str(file_path)) duration = float(probe["format"]["duration"]) return int(duration * 1000) # Convert to milliseconds - except (ffmpeg.Error, KeyError, ValueError, TypeError, Exception) as e: + except (ffmpeg.Error, KeyError, ValueError, TypeError, Exception) as e: # noqa: BLE001 logger.warning("Failed to get duration for %s: %s", file_path, e) return 0