Files
rehearshalhub/api/src/rehearsalhub/services/song.py
Mistral Vibe 02fd556372 feat: remove global Nextcloud config, enforce member-specific storage providers
- Remove global Nextcloud settings from config
- Make NextcloudClient require explicit credentials
- Update for_member() to return None when no credentials
- Modify services to accept optional storage client
- Update routers to pass member storage to services
- Add 403 responses when no storage provider configured
- Update internal endpoints to use member storage credentials

This change enforces that each member must configure their own
Nextcloud storage provider. If no provider is configured,
file operations will return 403 FORBIDDEN instead of falling
back to global placeholders.
2026-03-29 20:06:12 +02:00

93 lines
3.2 KiB
Python

from __future__ import annotations
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from rehearsalhub.db.models import AudioVersion, Song
from rehearsalhub.queue.redis_queue import RedisJobQueue
from rehearsalhub.repositories.audio_version import AudioVersionRepository
from rehearsalhub.repositories.song import SongRepository
from rehearsalhub.schemas.audio_version import AudioVersionCreate
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate
from rehearsalhub.storage.nextcloud import NextcloudClient
class SongService:
def __init__(
self,
session: AsyncSession,
job_queue: RedisJobQueue | None = None,
storage: NextcloudClient | None = None,
) -> None:
self._repo = SongRepository(session)
self._version_repo = AudioVersionRepository(session)
self._session = session
self._queue = job_queue or RedisJobQueue(session)
self._storage = storage
async def create_song(
self, band_id: uuid.UUID, data: SongCreate, creator_id: uuid.UUID, band_slug: str,
creator: object | None = None,
) -> Song:
from rehearsalhub.storage.nextcloud import NextcloudClient
nc_folder = f"bands/{band_slug}/songs/{data.title.lower().replace(' ', '-')}/"
storage = NextcloudClient.for_member(creator) if creator else self._storage
try:
await storage.create_folder(nc_folder)
except Exception:
nc_folder = None # best-effort
song = await self._repo.create(
band_id=band_id,
title=data.title,
status=data.status,
notes=data.notes,
nc_folder_path=nc_folder,
created_by=creator_id,
)
return song
async def list_songs(self, band_id: uuid.UUID) -> list[SongRead]:
songs = await self._repo.list_for_band(band_id)
result = []
for song in songs:
versions = song.versions
read = SongRead.model_validate(song)
read.version_count = len(versions)
if versions:
latest = max(versions, key=lambda v: v.version_number)
read.latest_version_id = latest.id
result.append(read)
return result
async def register_version(
self,
song_id: uuid.UUID,
data: AudioVersionCreate,
uploader_id: uuid.UUID,
) -> AudioVersion:
if data.nc_file_etag:
existing = await self._version_repo.get_by_etag(data.nc_file_etag)
if existing:
return existing
version_number = await self._repo.next_version_number(song_id)
version = await self._version_repo.create(
song_id=song_id,
version_number=version_number,
nc_file_path=data.nc_file_path,
nc_file_etag=data.nc_file_etag,
label=data.label,
format=data.format,
file_size_bytes=data.file_size_bytes,
analysis_status="pending",
uploaded_by=uploader_id,
)
await self._queue.enqueue(
"transcode",
{"version_id": str(version.id), "nc_file_path": data.nc_file_path},
)
return version