Add a mini-player to build mode

Every song in the Build tab gets a play button. A bar along the bottom
plays the folder in order with seeking, previous/next, volume (kept per
browser) and space to pause, and says so when the browser can't play a
format. Songs stream from the master library through a new endpoint with
range requests; it only serves regular music files inside the library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-30 17:14:17 -07:00
1 parent da74756c24
commit 491d679233
7 files changed
+443 -4

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
package engine
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/cdnutter/playlink/internal/fsops"
)
// OpenSong opens a song in one library for listening (the build mode
// player). Only regular music files inside the library are served: no
// symlinks, no other file types.
func (e *Engine) OpenSong(ctx context.Context, pairID int64, side, rel string) (*os.File, os.FileInfo, error) {
l, err := e.loadPair(ctx, pairID)
if err != nil {
return nil, nil, err
}
root, err := sideRoot(l, side)
if err != nil {
return nil, nil, err
}
abs, err := root.Resolve(rel)
if err != nil {
return nil, nil, err
}
if !l.isMusic(filepath.Base(abs)) {
return nil, nil, fmt.Errorf("%w: not a music file", ErrInvalidBulk)
}
if _, err := fsops.RegularFile(abs); err != nil {
return nil, nil, err
}
f, err := os.Open(abs)
if err != nil {
return nil, nil, err
}
// What was opened must still be the regular file that was checked, not
// something swapped in for it since.
fi, err := f.Stat()
li, lerr := os.Lstat(abs)
if err != nil || lerr != nil || !li.Mode().IsRegular() || !os.SameFile(fi, li) {
f.Close()
return nil, nil, errors.New("the file changed while opening it")
}
return f, fi, nil
}