From ba50a95f9942e861d7dfc3e55c3966ae2a68af92 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Tue, 9 Sep 2025 21:10:35 +0200 Subject: [PATCH] =?UTF-8?q?Mise=20=C3=A0=20jour=20.gitignore=20et=20retrai?= =?UTF-8?q?t=20du=20suivi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes .gitignore | 4 + pmocover/cache.go | 82 ++++- pmocover/db.go | 6 + pmodidl/didilite.go | 281 ++++++++++++++++++ {didl => pmodidl}/markdown.go | 2 +- {didl => pmodidl}/model.go | 3 +- {didl => pmodidl}/parser.go | 11 +- .../AVTransport/sv_AVTransportURIMetaData.go | 4 +- 9 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 pmodidl/didilite.go rename {didl => pmodidl}/markdown.go (99%) rename {didl => pmodidl}/model.go (98%) rename {didl => pmodidl}/parser.go (52%) diff --git a/.DS_Store b/.DS_Store index 7c893150dc3a22bb56b525a022fb0aae7d5ea199..a726a307dc60f7db789ac4d9cd6732b96b7565b8 100644 GIT binary patch delta 118 zcmZoMXfc=|&e%S&P;8=}q9`*10|O%ig8&0VE<-6pF+(OpGDG~tgX)GL2>}K@h612O zKC&cGK|VtnLn=cNLvdMfQC?1dUOLE-jXmD%o7p)8I2hYE9{kQcnO{Vgkzuo=$YJJ* G4J-ilQ5*mO delta 76 zcmZoMXfc=|&e%4wP;8=}q9_vs0|O%ig8&0V5knqBB129}adOVYkLr^RL|8WN_h6sI gXt0@`Lx6*^ZR5x9%#-;=bXkGQ84hf=6*fB*mh diff --git a/.gitignore b/.gitignore index 7a8449a9..9cf3c556 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ xxx /dcai/ ***/.pmomusic.yml +***/.pmomusic_covers/*** +***/.DS_Strore/*** +.DS_Strore/*** +.pmomusic_covers/*** \ No newline at end of file diff --git a/pmocover/cache.go b/pmocover/cache.go index 467ba5cd..2dea9cba 100644 --- a/pmocover/cache.go +++ b/pmocover/cache.go @@ -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 +} diff --git a/pmocover/db.go b/pmocover/db.go index cb06a71c..2fba85ac 100644 --- a/pmocover/db.go +++ b/pmocover/db.go @@ -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 +} diff --git a/pmodidl/didilite.go b/pmodidl/didilite.go new file mode 100644 index 00000000..978a7969 --- /dev/null +++ b/pmodidl/didilite.go @@ -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 +} diff --git a/didl/markdown.go b/pmodidl/markdown.go similarity index 99% rename from didl/markdown.go rename to pmodidl/markdown.go index 589f943a..3ee568c2 100644 --- a/didl/markdown.go +++ b/pmodidl/markdown.go @@ -1,4 +1,4 @@ -package didl +package pmodidl import ( "fmt" diff --git a/didl/model.go b/pmodidl/model.go similarity index 98% rename from didl/model.go rename to pmodidl/model.go index 5dad7d49..faaaef4c 100644 --- a/didl/model.go +++ b/pmodidl/model.go @@ -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"` diff --git a/didl/parser.go b/pmodidl/parser.go similarity index 52% rename from didl/parser.go rename to pmodidl/parser.go index 08297f87..a8a7ebc4 100644 --- a/didl/parser.go +++ b/pmodidl/parser.go @@ -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 } diff --git a/pmoupnp/devices/mediarenderer/AVTransport/sv_AVTransportURIMetaData.go b/pmoupnp/devices/mediarenderer/AVTransport/sv_AVTransportURIMetaData.go index 28f16b7f..258464c7 100644 --- a/pmoupnp/devices/mediarenderer/AVTransport/sv_AVTransportURIMetaData.go +++ b/pmoupnp/devices/mediarenderer/AVTransport/sv_AVTransportURIMetaData.go @@ -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 }