diff --git a/api/src/rehearsalhub/routers/bands.py b/api/src/rehearsalhub/routers/bands.py index 5ad7731..f727a84 100644 --- a/api/src/rehearsalhub/routers/bands.py +++ b/api/src/rehearsalhub/routers/bands.py @@ -6,7 +6,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from rehearsalhub.db.engine import get_session from rehearsalhub.db.models import Member from rehearsalhub.dependencies import get_current_member -from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers +from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate +from rehearsalhub.repositories.band import BandRepository from rehearsalhub.services.band import BandService router = APIRouter(prefix="/bands", tags=["bands"]) @@ -17,7 +18,6 @@ async def list_bands( session: AsyncSession = Depends(get_session), current_member: Member = Depends(get_current_member), ): - from rehearsalhub.repositories.band import BandRepository repo = BandRepository(session) bands = await repo.list_for_member(current_member.id) return [BandRead.model_validate(b) for b in bands] @@ -53,3 +53,29 @@ async def get_band( if band is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Band not found") return BandReadWithMembers.model_validate(band) + + +@router.patch("/{band_id}", response_model=BandRead) +async def update_band( + band_id: uuid.UUID, + data: BandUpdate, + session: AsyncSession = Depends(get_session), + current_member: Member = Depends(get_current_member), +): + repo = BandRepository(session) + role = await repo.get_member_role(band_id, current_member.id) + if role != "admin": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin only") + + band = await repo.get_by_id(band_id) + if band is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Band not found") + + updates: dict = {} + if data.nc_folder_path is not None: + path = data.nc_folder_path.strip() + updates["nc_folder_path"] = (path.rstrip("/") + "/") if path else None + + if updates: + band = await repo.update(band, **updates) + return BandRead.model_validate(band) diff --git a/api/src/rehearsalhub/schemas/band.py b/api/src/rehearsalhub/schemas/band.py index 6388ba6..238db6f 100644 --- a/api/src/rehearsalhub/schemas/band.py +++ b/api/src/rehearsalhub/schemas/band.py @@ -21,6 +21,10 @@ class BandCreate(BaseModel): nc_base_path: str | None = None # e.g. "Bands/MyBand/" — defaults to "bands/{slug}/" +class BandUpdate(BaseModel): + nc_folder_path: str | None = None # update the Nextcloud base folder for scans + + class BandRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: uuid.UUID diff --git a/watcher/src/watcher/event_loop.py b/watcher/src/watcher/event_loop.py index 9d2136b..1e7267b 100644 --- a/watcher/src/watcher/event_loop.py +++ b/watcher/src/watcher/event_loop.py @@ -109,27 +109,25 @@ async def poll_once(nc_client: NextcloudWatcherClient, settings: WatcherSettings for activity in activities: activity_id = int(activity.get("activity_id", 0)) - subject = activity.get("subject", "") + activity_type = activity.get("type", "") + subject = activity.get("subject", "") # human-readable, for logging only raw_path = extract_nc_file_path(activity) log.debug( - "Activity id=%d subject=%r path=%r", - activity_id, subject, raw_path, + "Activity id=%d type=%r subject=%r path=%r", + activity_id, activity_type, subject, raw_path, ) # Advance the cursor regardless of whether we act on this event _last_activity_id = max(_last_activity_id, activity_id) - if subject not in _UPLOAD_SUBJECTS: - log.debug("Skipping activity %d: subject %r not a file upload event", activity_id, subject) - continue - if raw_path is None: log.debug("Skipping activity %d: no file path in payload", activity_id) continue nc_path = normalize_nc_path(raw_path, nc_client.username) + # Only care about audio files — skip everything else immediately if not is_audio_file(nc_path, settings.audio_extensions): log.debug( "Skipping activity %d: '%s' is not an audio file (ext: %s)", @@ -144,6 +142,10 @@ async def poll_once(nc_client: NextcloudWatcherClient, settings: WatcherSettings ) continue + if activity_type not in _UPLOAD_SUBJECTS: + log.debug("Skipping activity %d: type %r not a file upload event", activity_id, activity_type) + continue + log.info("Detected audio upload: %s (activity %d)", nc_path, activity_id) etag = await nc_client.get_file_etag(nc_path) success = await register_version_with_api(nc_path, etag, settings.api_url) diff --git a/web/index.html b/web/index.html index 40d5b2c..c99c1bc 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,13 @@
- + +Invite link (copied to clipboard, valid 72h):
-{inviteLink}
+ Invite link (copied to clipboard, valid 72h):
+{inviteLink}
@@ -152,25 +202,25 @@ export function BandPage() {
{members?.map((m) => (
{error}
} - +{error}
} + setTitle(e.target.value)} onKeyDown={(e) => e.key === "Enter" && title && createMutation.mutate()} - style={{ width: "100%", padding: "8px 12px", background: "#131720", border: "1px solid #1C2235", borderRadius: 6, color: "#E2E6F0", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }} + style={{ width: "100%", padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }} autoFocus /> -
- A folder bands/{band.slug}/songs/{title.toLowerCase().replace(/\s+/g, "-") || "…"}/ will be created in Nextcloud.
+
+ A folder bands/{band.slug}/songs/{title.toLowerCase().replace(/\s+/g, "-") || "…"}/ will be created in Nextcloud.
- No songs yet. Create one or scan Nextcloud to import from {band.nc_folder_path ?? `bands/${band.slug}/`}.
+
+ No songs yet. Create one or scan Nextcloud to import from {band.nc_folder_path ?? `bands/${band.slug}/`}.
{error}
} - +{error}
} + { setName(e.target.value); setSlug(e.target.value.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")); }} - style={{ width: "100%", padding: "8px 12px", background: "#131720", border: "1px solid #1C2235", borderRadius: 6, color: "#E2E6F0", marginBottom: 12, fontSize: 14, boxSizing: "border-box" }} + style={inputStyle} /> - + setSlug(e.target.value)} - style={{ width: "100%", padding: "8px 12px", background: "#131720", border: "1px solid #1C2235", borderRadius: 6, color: "#E2E6F0", marginBottom: 16, fontSize: 14, fontFamily: "monospace", boxSizing: "border-box" }} + style={{ ...inputStyle, fontFamily: "monospace", marginBottom: 16 }} /> -Band invite
+Band invite
{error && ( -- You've been invited to join a band as {invite.role}. +
+ You've been invited to join a band as {invite.role}.
-+
Expires {new Date(invite.expires_at).toLocaleDateString()} {invite.used_at && " · Already used"}
@@ -78,16 +78,16 @@ export function InvitePage() { ) : (Log in or register to accept this invite.
+Log in or register to accept this invite.
@@ -97,7 +97,7 @@ export function InvitePage() { )} {!done && !invite && !error && ( -Loading invite…
+Loading invite…
)}