feat: album covers with palettes and signed links (M2)
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Successful in 1m0s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 34s
ci / api / api (push) Successful in 26s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 34s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Successful in 1m0s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 34s
ci / api / api (push) Successful in 26s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 34s
- covers are taken from the embedded picture of the first track or a
cover/folder/front image in the album folder, converted by ffmpeg to
WebP 64/300/1200 and named by content hash, so albums sharing a cover
share the files; a full scan looks again for albums without one
- a 3-colour palette (dominant, second, most saturated accent) from a
deterministic k-means feeds placeholders, page tints and the wave
- /covers/{id} is public but HMAC-signed and immutable; nginx serves
the file via X-Accel-Redirect, X_ACCEL=false serves it from api
- the web client picks the size by display size and pixel density
- install.sh and update.sh generate STREAM_SIGNING_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a685b1b7ca
commit
8b76aa3964
22 files changed
+988
-166
No files matched your search
@@ -40,6 +40,13 @@ type API struct {
|
||||
InternalToken string `env:"INTERNAL_TOKEN"`
|
||||
FFprobePath string `env:"FFPROBE_PATH" envDefault:"ffprobe"`
|
||||
|
||||
// SigningKey — ключ подписи ссылок на обложки и стрим (base64, ≥ 16 байт). Пусто — случайный до перезапуска.
|
||||
SigningKey string `env:"STREAM_SIGNING_KEY"`
|
||||
CacheDir string `env:"CACHE_DIR" envDefault:"/cache"`
|
||||
FFmpegPath string `env:"FFMPEG_PATH" envDefault:"ffmpeg"`
|
||||
// XAccel — файлы отдаёт nginx по X-Accel-Redirect; false — для запуска api без nginx.
|
||||
XAccel bool `env:"X_ACCEL" envDefault:"true"`
|
||||
|
||||
MeiliURL string `env:"MEILI_URL" envDefault:"http://meilisearch:7700"`
|
||||
MeiliMasterKey string `env:"MEILI_MASTER_KEY"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package sign — HMAC-подписи ссылок, которые клиент открывает без заголовка Authorization
|
||||
// (обложки в <img>, стрим в <audio>; docs/decisions.md, ADR-006).
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Signer подписывает наборы строк ключом сервера.
|
||||
type Signer struct{ key []byte }
|
||||
|
||||
// New создаёт Signer. Пустой ключ — случайный: ссылки перестанут действовать после перезапуска.
|
||||
func New(keyBase64 string) (*Signer, error) {
|
||||
if strings.TrimSpace(keyBase64) == "" {
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Signer{key: key}, nil
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(keyBase64))
|
||||
if err != nil || len(key) < 16 {
|
||||
return nil, fmt.Errorf("sign: key must be base64 of at least 16 bytes")
|
||||
}
|
||||
return &Signer{key: key}, nil
|
||||
}
|
||||
|
||||
// Sign возвращает подпись частей (base64url, 22 символа — первые 16 байт HMAC-SHA256).
|
||||
func (s *Signer) Sign(parts ...string) string {
|
||||
m := hmac.New(sha256.New, s.key)
|
||||
for _, p := range parts {
|
||||
_, _ = m.Write([]byte(p))
|
||||
_, _ = m.Write([]byte{0}) // разделитель: ("ab","c") и ("a","bc") подписываются по-разному
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(m.Sum(nil)[:16])
|
||||
}
|
||||
|
||||
// Verify проверяет подпись за постоянное время.
|
||||
func (s *Signer) Verify(sig string, parts ...string) bool {
|
||||
return hmac.Equal([]byte(sig), []byte(s.Sign(parts...)))
|
||||
}
|
||||
Reference in new issue
Block a user