audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: PDS blob upload failure no longer blocks track creation (#1213)

* replace .actor.profile glue with bare authority → profile redirect

at://did:plc:xxx (no collection) now redirects to /u/{did}, which is
the natural way to link to a profile. removes the .actor.profile
special case that was doing the same thing with extra steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: PDS blob upload failure no longer blocks track creation

PDS upload is best-effort — any failure now falls back to plyr.fm
storage instead of killing the entire upload. users see a warning
toast and can migrate the track to their PDS later via the portal.

previously only PayloadTooLargeError (413) was caught; timeouts,
network errors, and auth failures would propagate and fail the upload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6
and committed by
GitHub
e1f1105f a4248f79

+37 -10
+23 -3
backend/src/backend/api/tracks/uploads.py
··· 8 8 from dataclasses import dataclass 9 9 from io import BytesIO 10 10 from pathlib import Path 11 - from typing import Annotated 11 + from typing import Annotated, Any 12 12 13 13 import aiofiles 14 14 import logfire ··· 241 241 blob_ref: BlobRef | None 242 242 cid: str | None 243 243 size: int | None 244 + warning: str | None = None 244 245 245 246 246 247 async def _try_upload_to_pds( ··· 303 304 ) 304 305 return PdsBlobResult(blob_ref=None, cid=None, size=None) 305 306 306 - # any other exception is unexpected - let it propagate to fail the upload 307 + except Exception as e: 308 + # any other failure (timeout, network, auth) — fall back to R2-only. 309 + # PDS upload is best-effort; users can migrate via the portal later. 310 + logfire.warning( 311 + "pds blob upload failed, falling back to plyr.fm storage", 312 + error=f"{type(e).__name__}: {e}", 313 + did=auth_session.did, 314 + ) 315 + return PdsBlobResult( 316 + blob_ref=None, 317 + cid=None, 318 + size=None, 319 + warning="couldn't upload to your PDS — stored on plyr.fm instead. you can migrate it later on the portal page.", 320 + ) 307 321 308 322 309 323 async def _should_upload_pds_blob(db: AsyncSession, user_did: str) -> bool: ··· 888 902 # phase 7: post-upload tasks (tags, album sync, shared hooks) 889 903 await _schedule_post_upload(ctx, sr, track, run_hooks=published_by_us) 890 904 905 + result: dict[str, Any] = {"track_id": track.id} 906 + if pds_result and pds_result.warning: 907 + result["warnings"] = [pds_result.warning] 908 + 891 909 await job_service.update_progress( 892 910 ctx.upload_id, 893 911 JobStatus.COMPLETED, 894 912 "upload completed successfully", 895 - result={"track_id": track.id}, 913 + result=result, 896 914 ) 897 915 898 916 except UploadPhaseError as e: ··· 1124 1142 } 1125 1143 if job.result and "track_id" in job.result: 1126 1144 payload["track_id"] = job.result["track_id"] 1145 + if job.result and "warnings" in job.result: 1146 + payload["warnings"] = job.result["warnings"] 1127 1147 1128 1148 yield f"data: {json.dumps(payload)}\n\n" 1129 1149
+6
frontend/src/lib/uploader.svelte.ts
··· 189 189 label: 'view track', 190 190 href: `/track/${trackId}` 191 191 } : undefined); 192 + 193 + const warnings: string[] = update.warnings ?? []; 194 + for (const w of warnings) { 195 + toast.warning(w, 8000); 196 + } 197 + 192 198 tracksCache.invalidate(); 193 199 tracksCache.fetch(true); 194 200 if (onSuccess) {
+7 -6
frontend/src/routes/at/[...uri]/+page.server.ts
··· 24 24 throw error(400, 'invalid AT-URI'); 25 25 } 26 26 27 - if (!uri.collection || !uri.rkey) { 28 - throw error(400, 'AT-URI must include collection and rkey'); 27 + // bare authority (at://did:plc:xxx) → profile page 28 + if (!uri.collection) { 29 + throw redirect(301, `/u/${uri.hostname}`); 30 + } 31 + 32 + if (!uri.rkey) { 33 + throw error(400, 'AT-URI with collection must include rkey'); 29 34 } 30 35 31 36 // route by collection suffix — handles environment-aware namespaces ··· 50 55 } 51 56 const playlist: { id: number } = await response.json(); 52 57 throw redirect(301, `/playlist/${playlist.id}`); 53 - } 54 - 55 - if (uri.collection.endsWith('.actor.profile')) { 56 - throw redirect(301, `/u/${uri.hostname}`); 57 58 } 58 59 59 60 throw error(404, `unsupported collection: ${uri.collection}`);
+1 -1
loq.toml
··· 47 47 48 48 [[rules]] 49 49 path = "backend/src/backend/api/tracks/uploads.py" 50 - max_lines = 1157 50 + max_lines = 1167 51 51 52 52 [[rules]] 53 53 path = "backend/src/backend/config.py"