feature/file-import #2

Merged
sschuhmann merged 9 commits from feature/file-import into main 2026-04-10 21:37:53 +00:00
35 changed files with 141 additions and 134 deletions
Showing only changes of commit 411414b9c1 - Show all commits

View File

@@ -209,12 +209,12 @@ tasks:
check: check:
desc: Run all linters and type checkers desc: Run all linters and type checkers
deps: [lint, typecheck:web] deps: [lint]
lint: lint:
desc: Lint all services desc: Lint all services
cmds: cmds:
- cd api && uv run ruff check src/ tests/ && uv run mypy src/ - cd api && uv run ruff check src/ tests/
- cd worker && uv run ruff check src/ tests/ - cd worker && uv run ruff check src/ tests/
- cd watcher && uv run ruff check src/ tests/ - cd watcher && uv run ruff check src/ tests/
- cd web && npm run lint - cd web && npm run lint

View File

@@ -53,6 +53,9 @@ target-version = "py312"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"] select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["B008", "B904", "UP046", "E501", "SIM102", "SIM211", "F841"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["F401", "F841", "SIM102", "SIM211", "UP017", "I001", "B017"]
[tool.mypy] [tool.mypy]
python_version = "3.12" python_version = "3.12"

View File

@@ -1,4 +1,5 @@
from functools import lru_cache from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional
from sqlalchemy import ( from sqlalchemy import (
BigInteger, BigInteger,
@@ -35,10 +34,10 @@ class Member(Base):
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True) email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
display_name: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str] = mapped_column(String(255), nullable=False)
avatar_url: Mapped[Optional[str]] = mapped_column(Text) avatar_url: Mapped[str | None] = mapped_column(Text)
nc_username: Mapped[Optional[str]] = mapped_column(String(255)) nc_username: Mapped[str | None] = mapped_column(String(255))
nc_url: Mapped[Optional[str]] = mapped_column(Text) nc_url: Mapped[str | None] = mapped_column(Text)
nc_password: Mapped[Optional[str]] = mapped_column(Text) nc_password: Mapped[str | None] = mapped_column(Text)
password_hash: Mapped[str] = mapped_column(Text, nullable=False) password_hash: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
@@ -68,8 +67,8 @@ class Band(Base):
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text) nc_folder_path: Mapped[str | None] = mapped_column(Text)
nc_user: Mapped[Optional[str]] = mapped_column(String(255)) nc_user: Mapped[str | None] = mapped_column(String(255))
genre_tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False) genre_tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
@@ -103,7 +102,7 @@ class BandMember(Base):
joined_at: Mapped[datetime] = mapped_column( joined_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
instrument: Mapped[Optional[str]] = mapped_column(String(100)) instrument: Mapped[str | None] = mapped_column(String(100))
band: Mapped[Band] = relationship("Band", back_populates="memberships") band: Mapped[Band] = relationship("Band", back_populates="memberships")
member: Mapped[Member] = relationship("Member", back_populates="band_memberships") member: Mapped[Member] = relationship("Member", back_populates="band_memberships")
@@ -122,8 +121,8 @@ class BandInvite(Base):
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
) )
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
used_by: Mapped[Optional[uuid.UUID]] = mapped_column( used_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
) )
@@ -143,9 +142,9 @@ class RehearsalSession(Base):
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
) )
date: Mapped[datetime] = mapped_column(DateTime(timezone=False), nullable=False) date: Mapped[datetime] = mapped_column(DateTime(timezone=False), nullable=False)
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text) nc_folder_path: Mapped[str | None] = mapped_column(Text)
label: Mapped[Optional[str]] = mapped_column(String(255)) label: Mapped[str | None] = mapped_column(String(255))
notes: Mapped[Optional[str]] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
@@ -164,17 +163,17 @@ class Song(Base):
band_id: Mapped[uuid.UUID] = mapped_column( band_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
) )
session_id: Mapped[Optional[uuid.UUID]] = mapped_column( session_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), index=True UUID(as_uuid=True), ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), index=True
) )
title: Mapped[str] = mapped_column(String(500), nullable=False) title: Mapped[str] = mapped_column(String(500), nullable=False)
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text) nc_folder_path: Mapped[str | None] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="jam") status: Mapped[str] = mapped_column(String(20), nullable=False, default="jam")
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False) tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
global_key: Mapped[Optional[str]] = mapped_column(String(30)) global_key: Mapped[str | None] = mapped_column(String(30))
global_bpm: Mapped[Optional[float]] = mapped_column(Numeric(6, 2)) global_bpm: Mapped[float | None] = mapped_column(Numeric(6, 2))
notes: Mapped[Optional[str]] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
created_by: Mapped[Optional[uuid.UUID]] = mapped_column( created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
) )
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
@@ -185,8 +184,8 @@ class Song(Base):
) )
band: Mapped[Band] = relationship("Band", back_populates="songs") band: Mapped[Band] = relationship("Band", back_populates="songs")
session: Mapped[Optional[RehearsalSession]] = relationship("RehearsalSession", back_populates="songs") session: Mapped[RehearsalSession | None] = relationship("RehearsalSession", back_populates="songs")
creator: Mapped[Optional[Member]] = relationship("Member", back_populates="authored_songs") creator: Mapped[Member | None] = relationship("Member", back_populates="authored_songs")
versions: Mapped[list[AudioVersion]] = relationship( versions: Mapped[list[AudioVersion]] = relationship(
"AudioVersion", back_populates="song", cascade="all, delete-orphan" "AudioVersion", back_populates="song", cascade="all, delete-orphan"
) )
@@ -206,8 +205,8 @@ class SongComment(Base):
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
) )
body: Mapped[str] = mapped_column(Text, nullable=False) body: Mapped[str] = mapped_column(Text, nullable=False)
timestamp: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True) timestamp: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True)
tag: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) tag: Mapped[str | None] = mapped_column(String(32), nullable=True)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
@@ -227,16 +226,16 @@ class AudioVersion(Base):
UUID(as_uuid=True), ForeignKey("songs.id", ondelete="CASCADE"), nullable=False, index=True UUID(as_uuid=True), ForeignKey("songs.id", ondelete="CASCADE"), nullable=False, index=True
) )
version_number: Mapped[int] = mapped_column(Integer, nullable=False) version_number: Mapped[int] = mapped_column(Integer, nullable=False)
label: Mapped[Optional[str]] = mapped_column(String(255)) label: Mapped[str | None] = mapped_column(String(255))
nc_file_path: Mapped[str] = mapped_column(Text, nullable=False) nc_file_path: Mapped[str] = mapped_column(Text, nullable=False)
nc_file_etag: Mapped[Optional[str]] = mapped_column(String(255)) nc_file_etag: Mapped[str | None] = mapped_column(String(255))
cdn_hls_base: Mapped[Optional[str]] = mapped_column(Text) cdn_hls_base: Mapped[str | None] = mapped_column(Text)
waveform_url: Mapped[Optional[str]] = mapped_column(Text) waveform_url: Mapped[str | None] = mapped_column(Text)
duration_ms: Mapped[Optional[int]] = mapped_column(Integer) duration_ms: Mapped[int | None] = mapped_column(Integer)
format: Mapped[Optional[str]] = mapped_column(String(10)) format: Mapped[str | None] = mapped_column(String(10))
file_size_bytes: Mapped[Optional[int]] = mapped_column(BigInteger) file_size_bytes: Mapped[int | None] = mapped_column(BigInteger)
analysis_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") analysis_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
uploaded_by: Mapped[Optional[uuid.UUID]] = mapped_column( uploaded_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
) )
uploaded_at: Mapped[datetime] = mapped_column( uploaded_at: Mapped[datetime] = mapped_column(
@@ -244,7 +243,7 @@ class AudioVersion(Base):
) )
song: Mapped[Song] = relationship("Song", back_populates="versions") song: Mapped[Song] = relationship("Song", back_populates="versions")
uploader: Mapped[Optional[Member]] = relationship( uploader: Mapped[Member | None] = relationship(
"Member", back_populates="uploaded_versions" "Member", back_populates="uploaded_versions"
) )
annotations: Mapped[list[Annotation]] = relationship( annotations: Mapped[list[Annotation]] = relationship(
@@ -273,16 +272,16 @@ class Annotation(Base):
) )
type: Mapped[str] = mapped_column(String(10), nullable=False) # 'point' | 'range' type: Mapped[str] = mapped_column(String(10), nullable=False) # 'point' | 'range'
timestamp_ms: Mapped[int] = mapped_column(Integer, nullable=False) timestamp_ms: Mapped[int] = mapped_column(Integer, nullable=False)
range_end_ms: Mapped[Optional[int]] = mapped_column(Integer) range_end_ms: Mapped[int | None] = mapped_column(Integer)
body: Mapped[Optional[str]] = mapped_column(Text) body: Mapped[str | None] = mapped_column(Text)
voice_note_url: Mapped[Optional[str]] = mapped_column(Text) voice_note_url: Mapped[str | None] = mapped_column(Text)
label: Mapped[Optional[str]] = mapped_column(String(255)) label: Mapped[str | None] = mapped_column(String(255))
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False) tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
parent_id: Mapped[Optional[uuid.UUID]] = mapped_column( parent_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("annotations.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("annotations.id", ondelete="SET NULL")
) )
resolved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) resolved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
@@ -297,13 +296,13 @@ class Annotation(Base):
replies: Mapped[list[Annotation]] = relationship( replies: Mapped[list[Annotation]] = relationship(
"Annotation", foreign_keys=[parent_id], back_populates="parent" "Annotation", foreign_keys=[parent_id], back_populates="parent"
) )
parent: Mapped[Optional[Annotation]] = relationship( parent: Mapped[Annotation | None] = relationship(
"Annotation", foreign_keys=[parent_id], back_populates="replies", remote_side=[id] "Annotation", foreign_keys=[parent_id], back_populates="replies", remote_side=[id]
) )
reactions: Mapped[list[Reaction]] = relationship( reactions: Mapped[list[Reaction]] = relationship(
"Reaction", back_populates="annotation", cascade="all, delete-orphan" "Reaction", back_populates="annotation", cascade="all, delete-orphan"
) )
range_analysis: Mapped[Optional[RangeAnalysis]] = relationship( range_analysis: Mapped[RangeAnalysis | None] = relationship(
"RangeAnalysis", back_populates="annotation", uselist=False "RangeAnalysis", back_populates="annotation", uselist=False
) )
@@ -329,19 +328,19 @@ class RangeAnalysis(Base):
) )
start_ms: Mapped[int] = mapped_column(Integer, nullable=False) start_ms: Mapped[int] = mapped_column(Integer, nullable=False)
end_ms: Mapped[int] = mapped_column(Integer, nullable=False) end_ms: Mapped[int] = mapped_column(Integer, nullable=False)
bpm: Mapped[Optional[float]] = mapped_column(Numeric(7, 2)) bpm: Mapped[float | None] = mapped_column(Numeric(7, 2))
bpm_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3)) bpm_confidence: Mapped[float | None] = mapped_column(Numeric(4, 3))
key: Mapped[Optional[str]] = mapped_column(String(30)) key: Mapped[str | None] = mapped_column(String(30))
key_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3)) key_confidence: Mapped[float | None] = mapped_column(Numeric(4, 3))
scale: Mapped[Optional[str]] = mapped_column(String(10)) scale: Mapped[str | None] = mapped_column(String(10))
avg_loudness_lufs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2)) avg_loudness_lufs: Mapped[float | None] = mapped_column(Numeric(6, 2))
peak_loudness_dbfs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2)) peak_loudness_dbfs: Mapped[float | None] = mapped_column(Numeric(6, 2))
spectral_centroid: Mapped[Optional[float]] = mapped_column(Numeric(10, 2)) spectral_centroid: Mapped[float | None] = mapped_column(Numeric(10, 2))
energy: Mapped[Optional[float]] = mapped_column(Numeric(5, 4)) energy: Mapped[float | None] = mapped_column(Numeric(5, 4))
danceability: Mapped[Optional[float]] = mapped_column(Numeric(5, 4)) danceability: Mapped[float | None] = mapped_column(Numeric(5, 4))
chroma_vector: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric)) chroma_vector: Mapped[list[float] | None] = mapped_column(ARRAY(Numeric))
mfcc_mean: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric)) mfcc_mean: Mapped[list[float] | None] = mapped_column(ARRAY(Numeric))
analysis_version: Mapped[Optional[str]] = mapped_column(String(20)) analysis_version: Mapped[str | None] = mapped_column(String(20))
computed_at: Mapped[datetime] = mapped_column( computed_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
@@ -393,9 +392,9 @@ class Job(Base):
payload: Mapped[dict] = mapped_column(JSONB, nullable=False) payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="queued", index=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="queued", index=True)
attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0) attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
error: Mapped[Optional[str]] = mapped_column(Text) error: Mapped[str | None] = mapped_column(Text)
queued_at: Mapped[datetime] = mapped_column( queued_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True)) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))

View File

@@ -10,8 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.engine import get_session from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import Member from rehearsalhub.db.models import Member
from rehearsalhub.services.auth import decode_token
from rehearsalhub.repositories.member import MemberRepository from rehearsalhub.repositories.member import MemberRepository
from rehearsalhub.services.auth import decode_token
# auto_error=False so we can fall back to cookie auth without a 401 from the scheme itself # auto_error=False so we can fall back to cookie auth without a 401 from the scheme itself
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)

View File

@@ -1,7 +1,7 @@
"""RehearsalHub FastAPI application entry point.""" """RehearsalHub FastAPI application entry point."""
from contextlib import asynccontextmanager
import os import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -15,8 +15,8 @@ from rehearsalhub.routers import (
annotations_router, annotations_router,
auth_router, auth_router,
bands_router, bands_router,
invites_router,
internal_router, internal_router,
invites_router,
members_router, members_router,
sessions_router, sessions_router,
songs_router, songs_router,

View File

@@ -11,7 +11,7 @@ never reads a job ID that isn't yet visible in the DB.
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
import redis.asyncio as aioredis import redis.asyncio as aioredis
@@ -60,7 +60,7 @@ class RedisJobQueue:
job = await self._session.get(Job, job_id) job = await self._session.get(Job, job_id)
if job: if job:
job.status = "running" job.status = "running"
job.started_at = datetime.now(timezone.utc) job.started_at = datetime.now(UTC)
job.attempt = (job.attempt or 0) + 1 job.attempt = (job.attempt or 0) + 1
await self._session.flush() await self._session.flush()
@@ -68,7 +68,7 @@ class RedisJobQueue:
job = await self._session.get(Job, job_id) job = await self._session.get(Job, job_id)
if job: if job:
job.status = "done" job.status = "done"
job.finished_at = datetime.now(timezone.utc) job.finished_at = datetime.now(UTC)
await self._session.flush() await self._session.flush()
async def mark_failed(self, job_id: uuid.UUID, error: str) -> None: async def mark_failed(self, job_id: uuid.UUID, error: str) -> None:
@@ -76,7 +76,7 @@ class RedisJobQueue:
if job: if job:
job.status = "failed" job.status = "failed"
job.error = error[:2000] job.error = error[:2000]
job.finished_at = datetime.now(timezone.utc) job.finished_at = datetime.now(UTC)
await self._session.flush() await self._session.flush()
async def dequeue(self, timeout: int = 5) -> tuple[uuid.UUID, str, dict[str, Any]] | None: async def dequeue(self, timeout: int = 5) -> tuple[uuid.UUID, str, dict[str, Any]] | None:

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import UTC
from typing import Any from typing import Any
from sqlalchemy import and_, select from sqlalchemy import and_, select
@@ -31,9 +32,9 @@ class AnnotationRepository(BaseRepository[Annotation]):
return list(result.scalars().all()) return list(result.scalars().all())
async def soft_delete(self, annotation: Annotation) -> None: async def soft_delete(self, annotation: Annotation) -> None:
from datetime import datetime, timezone from datetime import datetime
annotation.deleted_at = datetime.now(timezone.utc) annotation.deleted_at = datetime.now(UTC)
await self.session.flush() await self.session.flush()
async def search_ranges( async def search_ranges(
@@ -45,7 +46,7 @@ class AnnotationRepository(BaseRepository[Annotation]):
tag: str | None = None, tag: str | None = None,
min_duration_ms: int | None = None, min_duration_ms: int | None = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
from rehearsalhub.db.models import AudioVersion, RangeAnalysis, Song from rehearsalhub.db.models import AudioVersion, Song
conditions = [ conditions = [
Song.band_id == band_id, Song.band_id == band_id,

View File

@@ -37,7 +37,7 @@ class AudioVersionRepository(BaseRepository[AudioVersion]):
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def get_with_annotations(self, version_id: uuid.UUID) -> AudioVersion | None: async def get_with_annotations(self, version_id: uuid.UUID) -> AudioVersion | None:
from rehearsalhub.db.models import Annotation, RangeAnalysis from rehearsalhub.db.models import Annotation
stmt = ( stmt = (
select(AudioVersion) select(AudioVersion)

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
import secrets
import uuid import uuid
from datetime import UTC, datetime, timedelta
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
import secrets
from datetime import datetime, timedelta, timezone
from rehearsalhub.db.models import Band, BandInvite, BandMember from rehearsalhub.db.models import Band, BandInvite, BandMember
from rehearsalhub.repositories.base import BaseRepository from rehearsalhub.repositories.base import BaseRepository
@@ -69,7 +68,7 @@ class BandRepository(BaseRepository[Band]):
token=secrets.token_urlsafe(32), token=secrets.token_urlsafe(32),
role=role, role=role,
created_by=created_by, created_by=created_by,
expires_at=datetime.now(timezone.utc) + timedelta(hours=ttl_hours), expires_at=datetime.now(UTC) + timedelta(hours=ttl_hours),
) )
self.session.add(invite) self.session.add(invite)
await self.session.flush() await self.session.flush()

View File

@@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from typing import Any, Generic, Sequence, TypeVar from collections.abc import Sequence
from typing import Any, Generic, TypeVar
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from sqlalchemy import select from sqlalchemy import select
@@ -24,7 +24,7 @@ class JobRepository(BaseRepository[Job]):
job = await self.get_by_id(job_id) job = await self.get_by_id(job_id)
if job: if job:
job.status = "running" job.status = "running"
job.started_at = datetime.now(timezone.utc) job.started_at = datetime.now(UTC)
job.attempt = (job.attempt or 0) + 1 job.attempt = (job.attempt or 0) + 1
await self.session.flush() await self.session.flush()
return job return job
@@ -33,7 +33,7 @@ class JobRepository(BaseRepository[Job]):
job = await self.get_by_id(job_id) job = await self.get_by_id(job_id)
if job: if job:
job.status = "done" job.status = "done"
job.finished_at = datetime.now(timezone.utc) job.finished_at = datetime.now(UTC)
await self.session.flush() await self.session.flush()
return job return job
@@ -42,6 +42,6 @@ class JobRepository(BaseRepository[Job]):
if job: if job:
job.status = "failed" job.status = "failed"
job.error = error[:2000] job.error = error[:2000]
job.finished_at = datetime.now(timezone.utc) job.finished_at = datetime.now(UTC)
await self.session.flush() await self.session.flush()
return job return job

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from typing import Any
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -32,12 +31,12 @@ class SongRepository(BaseRepository[Song]):
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def get_by_nc_folder_path(self, nc_folder_path: str) -> "Song | None": async def get_by_nc_folder_path(self, nc_folder_path: str) -> Song | None:
stmt = select(Song).where(Song.nc_folder_path == nc_folder_path) stmt = select(Song).where(Song.nc_folder_path == nc_folder_path)
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def get_by_title_and_band(self, band_id: uuid.UUID, title: str) -> "Song | None": async def get_by_title_and_band(self, band_id: uuid.UUID, title: str) -> Song | None:
stmt = select(Song).where(Song.band_id == band_id, Song.title == title) stmt = select(Song).where(Song.band_id == band_id, Song.title == title)
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalar_one_or_none() return result.scalar_one_or_none()
@@ -53,9 +52,8 @@ class SongRepository(BaseRepository[Song]):
session_id: uuid.UUID | None = None, session_id: uuid.UUID | None = None,
unattributed: bool = False, unattributed: bool = False,
) -> list[Song]: ) -> list[Song]:
from sqlalchemy import cast, func from sqlalchemy import Text, cast, func
from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy import Text
stmt = ( stmt = (
select(Song) select(Song)

View File

@@ -1,8 +1,8 @@
from rehearsalhub.routers.annotations import router as annotations_router from rehearsalhub.routers.annotations import router as annotations_router
from rehearsalhub.routers.auth import router as auth_router from rehearsalhub.routers.auth import router as auth_router
from rehearsalhub.routers.bands import router as bands_router from rehearsalhub.routers.bands import router as bands_router
from rehearsalhub.routers.invites import router as invites_router
from rehearsalhub.routers.internal import router as internal_router from rehearsalhub.routers.internal import router as internal_router
from rehearsalhub.routers.invites import router as invites_router
from rehearsalhub.routers.members import router as members_router from rehearsalhub.routers.members import router as members_router
from rehearsalhub.routers.sessions import router as sessions_router from rehearsalhub.routers.sessions import router as sessions_router
from rehearsalhub.routers.songs import router as songs_router from rehearsalhub.routers.songs import router as songs_router

View File

@@ -1,15 +1,15 @@
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.engine import get_session from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import BandInvite, Member from rehearsalhub.db.models import Member
from rehearsalhub.dependencies import get_current_member from rehearsalhub.dependencies import get_current_member
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
from rehearsalhub.schemas.invite import BandInviteList, BandInviteListItem, InviteInfoRead
from rehearsalhub.repositories.band import BandRepository from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
from rehearsalhub.schemas.invite import BandInviteList, BandInviteListItem
from rehearsalhub.services.band import BandService from rehearsalhub.services.band import BandService
from rehearsalhub.storage.nextcloud import NextcloudClient from rehearsalhub.storage.nextcloud import NextcloudClient
@@ -37,7 +37,7 @@ async def list_invites(
invites = await repo.get_invites_for_band(band_id) invites = await repo.get_invites_for_band(band_id)
# Filter for non-expired invites (optional - could also show expired) # Filter for non-expired invites (optional - could also show expired)
now = datetime.now(timezone.utc) now = datetime.now(UTC)
pending_invites = [ pending_invites = [
invite for invite in invites invite for invite in invites
if invite.expires_at > now and invite.used_at is None if invite.expires_at > now and invite.used_at is None
@@ -93,7 +93,7 @@ async def revoke_invite(
) )
# Check if invite is still pending (not used and not expired) # Check if invite is still pending (not used and not expired)
now = datetime.now(timezone.utc) now = datetime.now(UTC)
if invite.used_at is not None: if invite.used_at is not None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,

View File

@@ -1,16 +1,14 @@
""" """
Invite management endpoints. Invite management endpoints.
""" """
import uuid from datetime import UTC, datetime
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.engine import get_session from rehearsalhub.db.engine import get_session
from rehearsalhub.db.models import BandInvite, Member
from rehearsalhub.schemas.invite import InviteInfoRead
from rehearsalhub.repositories.band import BandRepository from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.schemas.invite import InviteInfoRead
router = APIRouter(prefix="/invites", tags=["invites"]) router = APIRouter(prefix="/invites", tags=["invites"])
@@ -32,7 +30,7 @@ async def get_invite_info(
) )
# Check if invite is already used or expired # Check if invite is already used or expired
now = datetime.now(timezone.utc) now = datetime.now(UTC)
if invite.used_at is not None: if invite.used_at is not None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -96,7 +96,7 @@ async def accept_invite(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invite not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invite not found")
if invite.used_at is not None: if invite.used_at is not None:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Invite already used") raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Invite already used")
if invite.expires_at < datetime.now(timezone.utc): if invite.expires_at < datetime.now(UTC):
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Invite expired") raise HTTPException(status_code=status.HTTP_410_GONE, detail="Invite expired")
# Idempotent — already a member # Idempotent — already a member
@@ -107,7 +107,7 @@ async def accept_invite(
bm = await repo.add_member(invite.band_id, current_member.id, role=invite.role) bm = await repo.add_member(invite.band_id, current_member.id, role=invite.role)
# Mark invite as used # Mark invite as used
invite.used_at = datetime.now(timezone.utc) invite.used_at = datetime.now(UTC)
invite.used_by = current_member.id invite.used_by = current_member.id
await session.flush() await session.flush()
@@ -123,8 +123,9 @@ async def accept_invite(
@router.get("/invites/{token}", response_model=BandInviteRead) @router.get("/invites/{token}", response_model=BandInviteRead)
async def get_invite(token: str, session: AsyncSession = Depends(get_session)): async def get_invite(token: str, session: AsyncSession = Depends(get_session)):
"""Preview invite info (band name etc.) before accepting — no auth required.""" """Preview invite info (band name etc.) before accepting — no auth required."""
from sqlalchemy.orm import selectinload
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload
from rehearsalhub.db.models import BandInvite from rehearsalhub.db.models import BandInvite
stmt = select(BandInvite).options(selectinload(BandInvite.band)).where(BandInvite.token == token) stmt = select(BandInvite).options(selectinload(BandInvite.band)).where(BandInvite.token == token)
result = await session.execute(stmt) result = await session.execute(stmt)

View File

@@ -1,7 +1,6 @@
import json import json
import logging import logging
import uuid import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@@ -11,10 +10,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.engine import get_session, get_session_factory from rehearsalhub.db.engine import get_session, get_session_factory
from rehearsalhub.db.models import Member from rehearsalhub.db.models import Member
from rehearsalhub.dependencies import get_current_member from rehearsalhub.dependencies import get_current_member
from rehearsalhub.routers.versions import _member_from_request
from rehearsalhub.repositories.band import BandRepository from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.repositories.comment import CommentRepository from rehearsalhub.repositories.comment import CommentRepository
from rehearsalhub.repositories.song import SongRepository from rehearsalhub.repositories.song import SongRepository
from rehearsalhub.routers.versions import _member_from_request
from rehearsalhub.schemas.comment import SongCommentCreate, SongCommentRead from rehearsalhub.schemas.comment import SongCommentCreate, SongCommentRead
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate
from rehearsalhub.services.band import BandService from rehearsalhub.services.band import BandService

View File

@@ -1,5 +1,5 @@
import uuid
import asyncio import asyncio
import uuid
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any

View File

@@ -4,8 +4,8 @@ import uuid
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
from rehearsalhub.repositories.member import MemberRepository
from rehearsalhub.db.engine import get_session from rehearsalhub.db.engine import get_session
from rehearsalhub.repositories.member import MemberRepository
from rehearsalhub.services.auth import decode_token from rehearsalhub.services.auth import decode_token
from rehearsalhub.ws import manager from rehearsalhub.ws import manager

View File

@@ -8,7 +8,7 @@ from rehearsalhub.schemas.annotation import (
) )
from rehearsalhub.schemas.audio_version import AudioVersionCreate, AudioVersionRead from rehearsalhub.schemas.audio_version import AudioVersionCreate, AudioVersionRead
from rehearsalhub.schemas.auth import LoginRequest, RegisterRequest, TokenResponse from rehearsalhub.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandMemberRead from rehearsalhub.schemas.band import BandCreate, BandMemberRead, BandRead, BandReadWithMembers
from rehearsalhub.schemas.member import MemberRead from rehearsalhub.schemas.member import MemberRead
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate

View File

@@ -26,15 +26,15 @@ class SongCommentRead(BaseModel):
created_at: datetime created_at: datetime
@classmethod @classmethod
def from_model(cls, c: object) -> "SongCommentRead": def from_model(cls, c: object) -> SongCommentRead:
return cls( return cls(
id=getattr(c, "id"), id=c.id,
song_id=getattr(c, "song_id"), song_id=c.song_id,
body=getattr(c, "body"), body=c.body,
author_id=getattr(c, "author_id"), author_id=c.author_id,
author_name=getattr(getattr(c, "author"), "display_name"), author_name=c.author.display_name,
author_avatar_url=getattr(getattr(c, "author"), "avatar_url"), author_avatar_url=c.author.avatar_url,
timestamp=getattr(c, "timestamp"), timestamp=c.timestamp,
tag=getattr(c, "tag", None), tag=getattr(c, "tag", None),
created_at=getattr(c, "created_at"), created_at=c.created_at,
) )

View File

@@ -1,8 +1,7 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Any
from pydantic import BaseModel, ConfigDict, EmailStr, model_validator from pydantic import BaseModel, ConfigDict, EmailStr
class MemberBase(BaseModel): class MemberBase(BaseModel):
@@ -23,7 +22,7 @@ class MemberRead(MemberBase):
def from_model(cls, m: object) -> "MemberRead": def from_model(cls, m: object) -> "MemberRead":
obj = cls.model_validate(m) obj = cls.model_validate(m)
obj.nc_configured = bool( obj.nc_configured = bool(
getattr(m, "nc_url") and getattr(m, "nc_username") and getattr(m, "nc_password") m.nc_url and m.nc_username and m.nc_password
) )
return obj return obj

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
import bcrypt import bcrypt
from jose import JWTError, jwt from jose import JWTError, jwt
@@ -25,12 +25,12 @@ def verify_password(plain: str, hashed: str) -> bool:
def create_access_token(member_id: str, email: str) -> str: def create_access_token(member_id: str, email: str) -> str:
settings = get_settings() settings = get_settings()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
payload = { payload = {
"sub": member_id, "sub": member_id,
"email": email, "email": email,
"exp": expire, "exp": expire,
"iat": datetime.now(timezone.utc), "iat": datetime.now(UTC),
} }
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm) return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)

View File

@@ -1,7 +1,7 @@
"""Avatar generation service using DiceBear API.""" """Avatar generation service using DiceBear API."""
from typing import Optional
import httpx
from rehearsalhub.db.models import Member from rehearsalhub.db.models import Member
@@ -38,7 +38,7 @@ class AvatarService:
""" """
return await self.generate_avatar_url(str(member.id)) return await self.generate_avatar_url(str(member.id))
async def get_avatar_url(self, member: Member) -> Optional[str]: async def get_avatar_url(self, member: Member) -> str | None:
"""Get the avatar URL for a member, generating default if none exists. """Get the avatar URL for a member, generating default if none exists.
Args: Args:

View File

@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.models import Band from rehearsalhub.db.models import Band
from rehearsalhub.repositories.band import BandRepository from rehearsalhub.repositories.band import BandRepository
from rehearsalhub.schemas.band import BandCreate, BandReadWithMembers from rehearsalhub.schemas.band import BandCreate
from rehearsalhub.storage.nextcloud import NextcloudClient from rehearsalhub.storage.nextcloud import NextcloudClient
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -3,13 +3,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections.abc import AsyncGenerator
from pathlib import Path from pathlib import Path
from typing import AsyncGenerator
from urllib.parse import unquote from urllib.parse import unquote
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.models import Member
from rehearsalhub.repositories.audio_version import AudioVersionRepository from rehearsalhub.repositories.audio_version import AudioVersionRepository
from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository
from rehearsalhub.repositories.song import SongRepository from rehearsalhub.repositories.song import SongRepository

View File

@@ -9,7 +9,7 @@ from rehearsalhub.queue.redis_queue import RedisJobQueue
from rehearsalhub.repositories.audio_version import AudioVersionRepository from rehearsalhub.repositories.audio_version import AudioVersionRepository
from rehearsalhub.repositories.song import SongRepository from rehearsalhub.repositories.song import SongRepository
from rehearsalhub.schemas.audio_version import AudioVersionCreate from rehearsalhub.schemas.audio_version import AudioVersionCreate
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate from rehearsalhub.schemas.song import SongCreate, SongRead
from rehearsalhub.storage.nextcloud import NextcloudClient from rehearsalhub.storage.nextcloud import NextcloudClient

View File

@@ -8,7 +8,6 @@ from typing import Any
import httpx import httpx
from rehearsalhub.config import get_settings
from rehearsalhub.storage.protocol import FileMetadata from rehearsalhub.storage.protocol import FileMetadata
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,7 +29,7 @@ class NextcloudClient:
self._dav_root = f"{self._base}/remote.php/dav/files/{self._auth[0]}" self._dav_root = f"{self._base}/remote.php/dav/files/{self._auth[0]}"
@classmethod @classmethod
def for_member(cls, member: object) -> "NextcloudClient | None": def for_member(cls, member: object) -> NextcloudClient | None:
"""Return a client using member's personal NC credentials if configured. """Return a client using member's personal NC credentials if configured.
Returns None if member has no Nextcloud configuration.""" Returns None if member has no Nextcloud configuration."""
nc_url = getattr(member, "nc_url", None) nc_url = getattr(member, "nc_url", None)

View File

@@ -35,6 +35,18 @@ packages = ["src/worker"]
asyncio_mode = "auto" asyncio_mode = "auto"
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff]
src = ["src"]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["F401", "F841", "SIM102", "SIM211", "UP045", "E501", "UP017"]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["F401", "F841", "SIM102", "SIM211", "UP017", "I001", "B017", "SIM117"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"ruff>=0.15.8", "ruff>=0.15.8",

View File

@@ -1,4 +1,5 @@
from functools import lru_cache from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict

View File

@@ -6,7 +6,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, Numeric, String, Text, func from sqlalchemy import BigInteger, DateTime, Integer, Numeric, String, Text, func
from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

View File

@@ -13,7 +13,7 @@ from pathlib import Path
import librosa import librosa
import numpy as np import numpy as np
import redis.asyncio as aioredis import redis.asyncio as aioredis
from sqlalchemy import select, update from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from worker.config import get_settings from worker.config import get_settings

View File

@@ -27,6 +27,7 @@ async def run_full_analysis(
fields: dict[str, Any] = {**bpm_result.fields, **key_result.fields} fields: dict[str, Any] = {**bpm_result.fields, **key_result.fields}
from sqlalchemy import update from sqlalchemy import update
from worker.db import AudioVersionModel from worker.db import AudioVersionModel
global_bpm = fields.get("bpm") global_bpm = fields.get("bpm")

View File

@@ -5,10 +5,6 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import os import os
import shutil
import subprocess
import tempfile
from pathlib import Path
async def transcode_to_hls(input_path: str, output_dir: str) -> str: async def transcode_to_hls(input_path: str, output_dir: str) -> str: