Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).
Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
is carried through share links and auto-selects audio + title + flag + about +
OG/meta on open.
GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
frequency bars; loop-filter rebuild makes generation ~25x faster.
Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
66fd78c10f
commit
a4384113c2
24 files changed
+1738
-258
No files matched your search
@@ -176,18 +176,65 @@ Rules that must never be violated:
|
||||
|
||||
**NAS is the primary storage backend. When NAS is reachable, every user file must end up on the NAS and be served from the NAS. When NAS is unreachable, files are stored locally and automatically migrated to NAS when it comes back online.**
|
||||
|
||||
File types and their NAS locations:
|
||||
#### Canonical storage layout — IDENTICAL on local disk and on the NAS
|
||||
|
||||
| File type | NAS path | Served via |
|
||||
**This is the single source-of-truth file structure. Both the local `storage/app/` cache and the NAS root MUST follow this exact same tree. Never invent a different layout for one or the other — any code that writes a user file (upload, edit, sync, fallback) must produce these paths verbatim, and `videos.path` / `video_audio_tracks.path` / `video_slides.filename` / etc. store the full `users/...` path identically whether the file currently lives on NAS or local.**
|
||||
|
||||
```
|
||||
users/{user-slug}/
|
||||
├── profile/
|
||||
│ ├── avatar.{ext}
|
||||
│ └── cover.{ext} ← banner (DB column is users.banner)
|
||||
├── playlists/
|
||||
│ └── {playlist-id}/
|
||||
│ └── thumb.{ext}
|
||||
├── posts/
|
||||
│ └── {post-id}/
|
||||
│ └── {filename} ← post images
|
||||
└── videos/
|
||||
└── {song-slug}/ ← ONE folder per song/video. EVERYTHING for it lives here.
|
||||
│ ┌──────────────── SOURCE OF TRUTH (synced to NAS) ───────┐
|
||||
├── {title-slug}.{ext} ← primary track / video file (canonical name)
|
||||
├── {song-slug}-{lang}-{id}.{ext} ← each extra-language audio track (one per language)
|
||||
├── slides/
|
||||
│ └── {position}.{ext} ← cover image(s) / slideshow frames
|
||||
├── thumb.{ext} ← cover for video-type uploads that have no slides
|
||||
├── meta.json ← {id, user_id, title, created_at}
|
||||
│ └──────────────────────────────────────────────────────┘
|
||||
└── cache/ ← REGENERABLE renders. LOCAL-ONLY. Never on NAS. Safe to wipe.
|
||||
├── video.mp4 ← generated "Download Video" (plain)
|
||||
├── video-viz.mp4 ← generated "Download Video" (visualizer)
|
||||
└── hls/
|
||||
└── {variant}/… ← adaptive-streaming rendition (.m3u8 + .ts)
|
||||
```
|
||||
|
||||
**Sources vs. the `cache/` subfolder — a hard rule:**
|
||||
- The song-folder **root** holds only the **source of truth** (primary track, extra-language tracks, slides, thumb, meta.json). These are what gets pushed to / pulled from the NAS.
|
||||
- **`cache/` holds only regenerable, derived renders** — the "Download Video" mp4s and the HLS rendition. It is **LOCAL-only and is NEVER pushed to the NAS** (`syncVideo()` pushes only the source files). Deleting `cache/` (or the whole subtree) is always safe — it rebuilds on the next download/stream. There must be **no shared `public/slideshow` or `public/hls` caches** anymore; every render lives under its song's `cache/`.
|
||||
- DB pointers: `videos.slideshow_video_path` → `users/.../{song}/cache/video.mp4`; `videos.hls_path` → `users/.../{song}/cache/hls`.
|
||||
- Reclaim space anytime with `php artisan nas:free-local-storage` (deletes song `cache/` folders); `tracks:reorganize` never treats anything under `cache/` as an orphan.
|
||||
|
||||
**Naming rules (apply on both local and NAS, always lowercase, Unicode-preserving slug via `NasSyncService::titleSlug()`):**
|
||||
- **One folder per song/video** at `users/{slug}/videos/{song-slug}/`. There is **NO `tracks/` subfolder** — every audio track (primary AND secondary) sits directly in this folder.
|
||||
- **Primary** keeps the canonical `{title-slug}.{ext}` (the name the NAS layer reconstructs in `uploadDirectToNas()` / `syncVideo()` — do not change this scheme).
|
||||
- **Each extra-language track** is `{song-slug}-{lang}-{db-track-id}.{ext}`. The DB id makes every track filename globally unique, so no two tracks can ever overwrite each other even within the same folder/language.
|
||||
- Slides are `slides/{position}.{ext}`.
|
||||
|
||||
File types and their canonical locations (same string on NAS and local):
|
||||
|
||||
| File type | Path (relative to NAS root and to `storage/app/`) | Served via |
|
||||
|---|---|---|
|
||||
| Video / audio | `users/{slug}/videos/{title-slug}/{title-slug}.{ext}` | `NasSyncService::ensureLocalCopy()` |
|
||||
| Video thumbnail | `users/{slug}/videos/{title-slug}/thumb.{ext}` | `MediaController::thumbnail` + `ensureLocalAsset()` |
|
||||
| Audio slides | `users/{slug}/videos/{title-slug}/slides/{n}.{ext}` | `MediaController::thumbnail` + `ensureLocalAsset()` |
|
||||
| Primary video / audio | `users/{slug}/videos/{song-slug}/{title-slug}.{ext}` | `NasSyncService::ensureLocalCopy()` |
|
||||
| Extra audio track | `users/{slug}/videos/{song-slug}/{song-slug}-{lang}-{track-id}.{ext}` | `NasSyncService::ensureLocalTrackCopy()` |
|
||||
| Video thumbnail | `users/{slug}/videos/{song-slug}/thumb.{ext}` | `MediaController::thumbnail` + `ensureLocalAsset()` |
|
||||
| Slides | `users/{slug}/videos/{song-slug}/slides/{n}.{ext}` | `MediaController::thumbnail` + `ensureLocalAsset()` |
|
||||
| Playlist thumbnail | `users/{slug}/playlists/{playlist-id}/thumb.{ext}` | `MediaController::thumbnail` + `ensureLocalAsset()` |
|
||||
| Avatar | `users/{slug}/profile/avatar.{ext}` | `MediaController::avatar` + `ensureLocalAsset()` |
|
||||
| Banner | `users/{slug}/profile/banner.{ext}` | `MediaController::banner` + `ensureLocalAsset()` |
|
||||
| Banner | `users/{slug}/profile/cover.{ext}` | `MediaController::banner` + `ensureLocalAsset()` |
|
||||
| Post images | `users/{slug}/posts/{post-id}/{filename}` | `MediaController::postImage` + `ensureLocalAsset()` |
|
||||
|
||||
The one-time migration `php artisan tracks:reorganize` enforces this layout for existing songs (dry-run by default; `--force` to apply).
|
||||
|
||||
The only files that live permanently on local disk are HLS segments (`storage/app/public/hls/{video_id}/`) because they are generated locally and served directly. Everything else is NAS.
|
||||
|
||||
**The following local directories must never exist as permanent storage.** They were deleted after migration and must not be recreated as destinations:
|
||||
|
||||
Reference in new issue
Block a user