Mise à jour .gitignore et retrait du suivi

This commit is contained in:
2025-09-09 21:10:35 +02:00
parent 0026cfe40d
commit ba50a95f99
9 changed files with 387 additions and 6 deletions

BIN
.DS_Store vendored

Binary file not shown.

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@
xxx
/dcai/
***/.pmomusic.yml
***/.pmomusic_covers/***
***/.DS_Strore/***
.DS_Strore/***
.pmomusic_covers/***

View File

@@ -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 lentré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 nont pas dentré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 dentrée en DB → supprimer le fichier
_ = os.Remove(f)
}
}
return nil
}

View File

@@ -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
View 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
}

View File

@@ -1,4 +1,4 @@
package didl
package pmodidl
import (
"fmt"

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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
}