Files
pmomusic/all.txt

416 lines
9.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
------- cache.go ------
package pmocover
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"errors"
"image"
"io"
"net/http"
"os"
"path/filepath"
"sync"
)
// CacheEntry représente une image stockée (original + dérivés)
type CacheEntry struct {
PK string `json:"pk"`
SourceURL string `json:"source_url"`
Hits int `json:"hits"`
LastUsed string `json:"last_used"` // ISO8601
}
// Cache persistant avec SQLite
type Cache struct {
dir string
limit int
db *DB
mu sync.Mutex
}
// NewCache ouvre ou crée un cache persistant avec SQLite
func NewCache(dir string, limit int) (*Cache, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
db, err := InitDB(dir)
if err != nil {
return nil, err
}
return &Cache{
dir: dir,
limit: limit,
db: db,
}, nil
}
// AddFromURL télécharge une image et la met en cache
func (c *Cache) AddFromURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New("bad status")
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return c.Add(url, data)
}
// Add ajoute une image déjà téléchargée
func (c *Cache) Add(url string, data []byte) (string, error) {
pk := pkFromURL(url)
origPath := filepath.Join(c.dir, pk+".orig.webp")
c.mu.Lock()
defer c.mu.Unlock()
if _, err := os.Stat(origPath); errors.Is(err, os.ErrNotExist) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return "", err
}
buf, err := encodeWebP(img)
if err != nil {
return "", err
}
if err := os.WriteFile(origPath, buf, 0o644); err != nil {
return "", err
}
}
_ = c.db.Add(pk, url) // ajoute ou met à jour la base
return pk, nil
}
// Get renvoie le chemin du fichier original et met à jour hits/last_used
func (c *Cache) Get(pk string) (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Get(pk)
if err != nil {
return "", err
}
_ = c.db.UpdateHit(pk)
origPath := filepath.Join(c.dir, pk+".orig.webp")
if _, err := os.Stat(origPath); err != nil {
return "", err
}
return origPath, nil
}
// Purge vide complètement le cache
func (c *Cache) Purge() error {
c.mu.Lock()
defer c.mu.Unlock()
files, _ := filepath.Glob(filepath.Join(c.dir, "*"))
for _, f := range files {
os.Remove(f)
}
return c.db.Purge()
}
// pkFromURL → hash stable basé sur lURL
func pkFromURL(url string) string {
h := sha1.Sum([]byte(url))
return hex.EncodeToString(h[:8])
}
-----------------
------- db.go ------
package pmocover
import (
"database/sql"
"path/filepath"
"time"
_ "modernc.org/sqlite"
)
// DB représente la base SQLite du cache
type DB struct {
conn *sql.DB
}
// InitDB ouvre ou crée la base SQLite dans le répertoire dir
func InitDB(dir string) (*DB, error) {
path := filepath.Join(dir, "cache.db")
conn, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
db := &DB{conn: conn}
if err := db.initTables(); err != nil {
conn.Close()
return nil, err
}
return db, nil
}
// initTables crée la table covers si elle nexiste pas
func (db *DB) initTables() error {
_, err := db.conn.Exec(`
CREATE TABLE IF NOT EXISTS covers (
pk TEXT PRIMARY KEY,
source_url TEXT,
hits INTEGER DEFAULT 0,
last_used TEXT
);
`)
return err
}
// Add ajoute ou met à jour une entrée cover
func (db *DB) Add(pk, url string) error {
_, err := db.conn.Exec(`
INSERT INTO covers(pk, source_url, hits, last_used)
VALUES(?, ?, 0, ?)
ON CONFLICT(pk) DO UPDATE SET
source_url=excluded.source_url,
last_used=excluded.last_used;
`, pk, url, time.Now().UTC().Format(time.RFC3339))
return err
}
// Get récupère une entrée
func (db *DB) Get(pk string) (*CacheEntry, error) {
row := db.conn.QueryRow(`
SELECT pk, source_url, hits, last_used
FROM covers
WHERE pk = ?
`, pk)
entry := &CacheEntry{}
var lastUsed sql.NullString
err := row.Scan(&entry.PK, &entry.SourceURL, &entry.Hits, &lastUsed)
if err != nil {
return nil, err
}
if lastUsed.Valid {
entry.LastUsed = lastUsed.String
}
return entry, nil
}
// UpdateHit incrémente hits et met à jour last_used
func (db *DB) UpdateHit(pk string) error {
_, err := db.conn.Exec(`
UPDATE covers
SET hits = hits + 1,
last_used = ?
WHERE pk = ?
`, time.Now().UTC().Format(time.RFC3339), pk)
return err
}
// Purge supprime toutes les entrées de la base
func (db *DB) Purge() error {
_, err := db.conn.Exec(`DELETE FROM covers`)
return err
}
// GetAll récupère toutes les entrées triées par hits décroissant
func (db *DB) GetAll() ([]*CacheEntry, error) {
rows, err := db.conn.Query(`
SELECT pk, source_url, hits, last_used
FROM covers
ORDER BY hits DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []*CacheEntry
for rows.Next() {
entry := &CacheEntry{}
var lastUsed sql.NullString
if err := rows.Scan(&entry.PK, &entry.SourceURL, &entry.Hits, &lastUsed); err != nil {
continue
}
if lastUsed.Valid {
entry.LastUsed = lastUsed.String
}
entries = append(entries, entry)
}
return entries, nil
}
-----------------
------- handler.go ------
package pmocover
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
// ServeMux branche les routes /covers
func (c *Cache) ServeMux(mux *http.ServeMux) {
// Images
mux.HandleFunc("/covers/images/", func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/covers/images/"), "/")
if len(parts) == 0 || parts[0] == "" {
http.Error(w, "missing pk", 400)
return
}
pk := parts[0]
if len(parts) == 1 {
path, err := c.Get(pk)
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "image/webp")
http.ServeFile(w, r, path)
return
}
size, err := strconv.Atoi(parts[1])
if err != nil {
http.Error(w, "invalid size", 400)
return
}
variantPath := filepath.Join(c.dir, pk+"."+parts[1]+".webp")
if _, err := os.Stat(variantPath); os.IsNotExist(err) {
data, err := c.generateVariant(pk, size)
if err != nil {
http.Error(w, "cannot generate", 500)
return
}
w.Header().Set("Content-Type", "image/webp")
w.Write(data)
return
}
w.Header().Set("Content-Type", "image/webp")
http.ServeFile(w, r, variantPath)
})
// Stats
mux.HandleFunc("/covers/stats", func(w http.ResponseWriter, r *http.Request) {
entries, _ := c.db.GetAll()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
})
}
-----------------
------- pmocover.go ------
package pmocover
import "gargoton.petite-maison-orange.fr/eric/pmomusic/pmoconfig"
// GetCoverCache récupère le cache, crée le dossier et la base si nécessaire
func GetCoverCache() (*Cache, error) {
dir := pmoconfig.GetConfig().GetCoverCacheDir()
cache, err := NewCache(dir, 2000)
if err != nil {
return nil, err
}
return cache, nil
}
-----------------
------- webp.go ------
package pmocover
import (
"bytes"
"image"
"image/draw"
"os"
"path/filepath"
"strconv"
"github.com/chai2010/webp"
xdraw "golang.org/x/image/draw"
)
// encodeWebP convertit une image.Image en WebP avec compression sans perte.
func encodeWebP(img image.Image) ([]byte, error) {
var buf bytes.Buffer
if err := webp.Encode(&buf, img, &webp.Options{Lossless: true}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ensureSquare génère une image carrée paddée transparente
func ensureSquare(img image.Image, size int) image.Image {
dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.Draw(dst, dst.Bounds(), &image.Uniform{C: image.Transparent}, image.Point{}, draw.Src)
srcBounds := img.Bounds()
srcW, srcH := srcBounds.Dx(), srcBounds.Dy()
var scale float64
if srcW > srcH {
scale = float64(size) / float64(srcW)
} else {
scale = float64(size) / float64(srcH)
}
newW, newH := int(float64(srcW)*scale), int(float64(srcH)*scale)
// redimensionnement avec Catmull-Rom
tmp := image.NewRGBA(image.Rect(0, 0, newW, newH))
xdraw.CatmullRom.Scale(tmp, tmp.Bounds(), img, srcBounds, xdraw.Over, nil)
// centrer l'image redimensionnée dans le carré
offset := image.Pt((size-newW)/2, (size-newH)/2)
draw.Draw(dst, tmp.Bounds().Add(offset), tmp, image.Point{}, draw.Over)
return dst
}
// generateVariant génère une version carrée en WebP pour une taille donnée.
// Si le fichier existe déjà, il est renvoyé directement.
func (c *Cache) generateVariant(pk string, size int) ([]byte, error) {
variantPath := filepath.Join(c.dir, pk+"."+strconv.Itoa(size)+".webp")
// si le variant existe déjà, le renvoyer
if data, err := os.ReadFile(variantPath); err == nil {
return data, nil
}
// lire l'image originale
origPath := filepath.Join(c.dir, pk+".orig.webp")
data, err := os.ReadFile(origPath)
if err != nil {
return nil, err
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, err
}
// générer une image carrée
sq := ensureSquare(img, size)
// encoder en WebP
buf, err := encodeWebP(sq)
if err != nil {
return nil, err
}
// sauvegarder le variant
if err := os.WriteFile(variantPath, buf, 0o644); err != nil {
return nil, err
}
return buf, nil
}
-----------------