Version fonctionnelle du cache #15
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@
|
||||
xxx
|
||||
/dcai/
|
||||
***/.pmomusic.yml
|
||||
***/.pmomusic_covers/***
|
||||
***/.DS_Strore/***
|
||||
.DS_Strore/***
|
||||
.pmomusic_covers/***
|
||||
BIN
.pmomusic_covers/bd89518e78a247d3.128.webp
Normal file
BIN
.pmomusic_covers/bd89518e78a247d3.128.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
.pmomusic_covers/bd89518e78a247d3.256.webp
Normal file
BIN
.pmomusic_covers/bd89518e78a247d3.256.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
.pmomusic_covers/bd89518e78a247d3.64.webp
Normal file
BIN
.pmomusic_covers/bd89518e78a247d3.64.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
BIN
.pmomusic_covers/bd89518e78a247d3.orig.webp
Normal file
BIN
.pmomusic_covers/bd89518e78a247d3.orig.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 496 KiB |
Binary file not shown.
@@ -5,12 +5,17 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg" // support JPEG
|
||||
_ "image/png" // support PNG
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
_ "golang.org/x/image/webp" // support WebP
|
||||
)
|
||||
|
||||
// CacheEntry représente une image stockée (original + dérivés)
|
||||
@@ -65,6 +70,34 @@ func (c *Cache) AddFromURL(url string) (string, error) {
|
||||
return c.Add(url, data)
|
||||
}
|
||||
|
||||
// EnsureFromURL vérifie si l'URL est déjà dans le cache et que le fichier existe.
|
||||
func (c *Cache) EnsureFromURL(url string) (string, error) {
|
||||
pk := pkFromURL(url)
|
||||
|
||||
// Vérifie si une entrée existe déjà en base
|
||||
_, err := c.db.Get(pk)
|
||||
if err == nil {
|
||||
// Vérifie aussi que le fichier original existe
|
||||
origPath := filepath.Join(c.dir, pk+".orig.webp")
|
||||
if _, statErr := os.Stat(origPath); statErr == nil {
|
||||
return pk, nil
|
||||
}
|
||||
// Si le fichier a disparu → on retélécharge
|
||||
}
|
||||
|
||||
// Sinon, on télécharge et on ajoute
|
||||
return c.AddFromURL(url)
|
||||
}
|
||||
|
||||
// decodeImage essaie de décoder une image depuis un []byte en utilisant les formats connus
|
||||
func decodeImage(data []byte) (image.Image, string, error) {
|
||||
img, format, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("échec de décodage image: %w", err)
|
||||
}
|
||||
return img, format, nil
|
||||
}
|
||||
|
||||
// Add ajoute une image déjà téléchargée
|
||||
func (c *Cache) Add(url string, data []byte) (string, error) {
|
||||
pk := pkFromURL(url)
|
||||
@@ -74,10 +107,12 @@ func (c *Cache) Add(url string, data []byte) (string, error) {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, err := os.Stat(origPath); errors.Is(err, os.ErrNotExist) {
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
img, format, err := decodeImage(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Printf("Décodage réussi : format %s depuis %s\n", format, url)
|
||||
|
||||
buf, err := encodeWebP(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -127,3 +162,48 @@ func pkFromURL(url string) string {
|
||||
h := sha1.Sum([]byte(url))
|
||||
return hex.EncodeToString(h[:8])
|
||||
}
|
||||
|
||||
// Consolidate parcourt la base et corrige les incohérences
|
||||
func (c *Cache) Consolidate() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entries, err := c.db.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Vérifie chaque entrée de la base
|
||||
for _, e := range entries {
|
||||
origPath := filepath.Join(c.dir, e.PK+".orig.webp")
|
||||
if _, err := os.Stat(origPath); os.IsNotExist(err) {
|
||||
// Fichier absent → on retente un download
|
||||
resp, err := http.Get(e.SourceURL)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
// Impossible de retélécharger → suppression de l’entrée
|
||||
_ = c.db.Delete(e.PK)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
if _, err := c.Add(e.SourceURL, data); err != nil {
|
||||
// Si on échoue quand même → suppression
|
||||
_ = c.db.Delete(e.PK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifie les fichiers orphelins (qui n’ont pas d’entrée en DB)
|
||||
files, _ := filepath.Glob(filepath.Join(c.dir, "*.orig.webp"))
|
||||
for _, f := range files {
|
||||
pk := filepath.Base(f)
|
||||
pk = pk[:len(pk)-len(".orig.webp")]
|
||||
_, err := c.db.Get(pk)
|
||||
if err != nil {
|
||||
// Pas d’entrée en DB → supprimer le fichier
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -116,3 +116,9 @@ func (db *DB) GetAll() ([]*CacheEntry, error) {
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// Delete supprime une entrée spécifique
|
||||
func (db *DB) Delete(pk string) error {
|
||||
_, err := db.conn.Exec(`DELETE FROM covers WHERE pk = ?`, pk)
|
||||
return err
|
||||
}
|
||||
|
||||
281
pmodidl/didilite.go
Normal file
281
pmodidl/didilite.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package pmodidl
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmocover"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Méthodes pour DIDLLite
|
||||
|
||||
// CacheAllCoverArts met en cache toutes les images de couverture et stocke les clés primaires
|
||||
func (d *DIDLLite) CacheAllCoverArts(cache *pmocover.Cache) error {
|
||||
itemsWithCovers := Filter(d.AllItems(), func(item *Item) bool {
|
||||
return item.AlbumArt != ""
|
||||
})
|
||||
|
||||
for item := range itemsWithCovers {
|
||||
pk, err := cache.EnsureFromURL(item.AlbumArt)
|
||||
if err != nil {
|
||||
log.Errorf("❌ Erreur lors de la mise en cache de %s: %v", item.AlbumArt, err)
|
||||
continue
|
||||
}
|
||||
log.Infof("✅ Mise en cache de %s : pk[%s]", item.AlbumArt, pk)
|
||||
item.AlbumArtPk = pk
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllContainers retourne un itérateur sur tous les containers de manière récursive
|
||||
func (d *DIDLLite) AllContainers() iter.Seq[*Container] {
|
||||
return func(yield func(*Container) bool) {
|
||||
for _, container := range d.Containers {
|
||||
if !yield(&container) {
|
||||
return
|
||||
}
|
||||
for child := range container.AllContainers() {
|
||||
if !yield(child) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AllItems retourne un itérateur sur tous les items de manière récursive
|
||||
func (d *DIDLLite) AllItems() iter.Seq[*Item] {
|
||||
return func(yield func(*Item) bool) {
|
||||
for _, item := range d.Items {
|
||||
if !yield(&item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for container := range d.AllContainers() {
|
||||
for _, item := range container.Items {
|
||||
if !yield(&item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetContainerByID retourne un itérateur sur les containers avec l'ID spécifié
|
||||
func (d *DIDLLite) GetContainerByID(id string) iter.Seq[*Container] {
|
||||
return func(yield func(*Container) bool) {
|
||||
for container := range d.AllContainers() {
|
||||
if container.ID == id {
|
||||
if !yield(container) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetItemByID retourne un itérateur sur les items avec l'ID spécifié
|
||||
func (d *DIDLLite) GetItemByID(id string) iter.Seq[*Item] {
|
||||
return func(yield func(*Item) bool) {
|
||||
for item := range d.AllItems() {
|
||||
if item.ID == id {
|
||||
if !yield(item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FilterContainers filtre les containers selon un prédicat
|
||||
func (d *DIDLLite) FilterContainers(predicate func(*Container) bool) iter.Seq[*Container] {
|
||||
return func(yield func(*Container) bool) {
|
||||
for container := range d.AllContainers() {
|
||||
if predicate(container) && !yield(container) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FilterItems filtre les items selon un prédicat
|
||||
func (d *DIDLLite) FilterItems(predicate func(*Item) bool) iter.Seq[*Item] {
|
||||
return func(yield func(*Item) bool) {
|
||||
for item := range d.AllItems() {
|
||||
if predicate(item) && !yield(item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes pour Container
|
||||
|
||||
// AllContainers retourne un itérateur sur tous les containers enfants de manière récursive
|
||||
func (c *Container) AllContainers() iter.Seq[*Container] {
|
||||
return func(yield func(*Container) bool) {
|
||||
if !yield(c) {
|
||||
return
|
||||
}
|
||||
for _, child := range c.Containers {
|
||||
for container := range child.AllContainers() {
|
||||
if !yield(container) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AllItems retourne un itérateur sur tous les items du container et de ses enfants
|
||||
func (c *Container) AllItems() iter.Seq[*Item] {
|
||||
return func(yield func(*Item) bool) {
|
||||
for _, item := range c.Items {
|
||||
if !yield(&item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, child := range c.Containers {
|
||||
for item := range child.AllItems() {
|
||||
if !yield(item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetChildContainers retourne un itérateur sur les containers enfants directs
|
||||
func (c *Container) GetChildContainers() iter.Seq[*Container] {
|
||||
return func(yield func(*Container) bool) {
|
||||
for _, container := range c.Containers {
|
||||
if !yield(&container) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetChildItems retourne un itérateur sur les items directs du container
|
||||
func (c *Container) GetChildItems() iter.Seq[*Item] {
|
||||
return func(yield func(*Item) bool) {
|
||||
for _, item := range c.Items {
|
||||
if !yield(&item) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes pour Item
|
||||
|
||||
// GetAudioResources retourne un itérateur sur les ressources audio
|
||||
func (i *Item) GetAudioResources() iter.Seq[Res] {
|
||||
return func(yield func(Res) bool) {
|
||||
for _, res := range i.Ress {
|
||||
if strings.HasPrefix(res.ProtocolInfo, "http-get:*:audio/") {
|
||||
if !yield(res) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPrimaryResource retourne la ressource principale (première disponible)
|
||||
func (i *Item) GetPrimaryResource() iter.Seq[Res] {
|
||||
return func(yield func(Res) bool) {
|
||||
if len(i.Ress) > 0 {
|
||||
if !yield(i.Ress[0]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetMetadata retourne un itérateur sur les métadonnées sous forme de paires clé-valeur
|
||||
func (i *Item) GetMetadata() iter.Seq2[string, string] {
|
||||
return func(yield func(string, string) bool) {
|
||||
if i.Title != "" && !yield("title", i.Title) {
|
||||
return
|
||||
}
|
||||
if i.Artist != "" && !yield("artist", i.Artist) {
|
||||
return
|
||||
}
|
||||
if i.Album != "" && !yield("album", i.Album) {
|
||||
return
|
||||
}
|
||||
if i.Genre != "" && !yield("genre", i.Genre) {
|
||||
return
|
||||
}
|
||||
if i.Date != "" && !yield("date", i.Date) {
|
||||
return
|
||||
}
|
||||
if i.OriginalTrackNumber != "" && !yield("trackNumber", i.OriginalTrackNumber) {
|
||||
return
|
||||
}
|
||||
for _, desc := range i.Descs {
|
||||
if desc.TrackGain != "" && !yield("replayGain", desc.TrackGain) {
|
||||
return
|
||||
}
|
||||
if desc.TrackPeak != "" && !yield("replayPeak", desc.TrackPeak) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fonctions utilitaires
|
||||
|
||||
// Filter filtre une séquence selon un prédicat
|
||||
func Filter[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
for value := range seq {
|
||||
if predicate(value) && !yield(value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map transforme une séquence en appliquant une fonction
|
||||
func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
for value := range seq {
|
||||
if !yield(f(value)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect collecte tous les éléments d'une séquence dans une slice
|
||||
func Collect[T any](seq iter.Seq[T]) []T {
|
||||
return slices.Collect(seq)
|
||||
}
|
||||
|
||||
// First retourne le premier élément d'une séquence
|
||||
func First[T any](seq iter.Seq[T]) (T, bool) {
|
||||
var zero T
|
||||
next, stop := iter.Pull(seq)
|
||||
defer stop()
|
||||
|
||||
value, ok := next()
|
||||
if !ok {
|
||||
return zero, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
// Count compte le nombre d'éléments dans une séquence
|
||||
func Count[T any](seq iter.Seq[T]) int {
|
||||
count := 0
|
||||
for range seq {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package didl
|
||||
package pmodidl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package didl
|
||||
package pmodidl
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
@@ -40,6 +40,7 @@ type Item struct {
|
||||
Album string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ album,omitempty"`
|
||||
Genre string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ genre,omitempty"`
|
||||
AlbumArt string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ albumArtURI,omitempty"`
|
||||
AlbumArtPk string
|
||||
Date string `xml:"http://purl.org/dc/elements/1.1/ date,omitempty"`
|
||||
OriginalTrackNumber string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ originalTrackNumber,omitempty"`
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package didl
|
||||
package pmodidl
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmocover"
|
||||
)
|
||||
|
||||
func Parse(metadata string) (*DIDLLite, error) {
|
||||
@@ -13,5 +15,12 @@ func Parse(metadata string) (*DIDLLite, error) {
|
||||
|
||||
}
|
||||
|
||||
cache, err := pmocover.GetCoverCache()
|
||||
if err != nil {
|
||||
return &didl, fmt.Errorf("failed to get cover cache: %v", err)
|
||||
}
|
||||
|
||||
didl.CacheAllCoverArts(cache)
|
||||
|
||||
return &didl, nil
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package avtransport
|
||||
|
||||
import (
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/didl"
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmodidl"
|
||||
sv "gargoton.petite-maison-orange.fr/eric/pmomusic/pmoupnp/devices/services/statevariables"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func _AVTransportURIMetaDataParser(value string) (interface{}, error) {
|
||||
log.Debug("[avtransport] Parsing AVTransport)")
|
||||
didl, err := didl.Parse(value)
|
||||
didl, err := pmodidl.Parse(value)
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user