Maintenant capter les URL des documents didl parser et les stocker dans le cache...

This commit is contained in:
2025-09-09 19:49:49 +02:00
parent 1c9755774b
commit d799a3fe38
14 changed files with 387 additions and 6 deletions

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
}

129
pmodidl/markdown.go Normal file
View File

@@ -0,0 +1,129 @@
package pmodidl
import (
"fmt"
"strings"
)
func (d *DIDLLite) ToMarkdown() string {
var buf strings.Builder
buf.WriteString("### DIDL-Lite Document\n\n")
if len(d.Containers) > 0 {
buf.WriteString("#### Containers\n\n")
for _, c := range d.Containers {
c.markdown(&buf, 0)
}
}
if len(d.Items) > 0 {
buf.WriteString("#### Items\n\n")
for _, i := range d.Items {
i.markdown(&buf, 0)
}
}
return buf.String()
}
func (c *Container) markdown(buf *strings.Builder, depth int) {
indent := strings.Repeat(" ", depth)
buf.WriteString(fmt.Sprintf("%s- **Container**: %s\n", indent, c.Title))
buf.WriteString(fmt.Sprintf("%s - ID: `%s`\n", indent, c.ID))
buf.WriteString(fmt.Sprintf("%s - ParentID: `%s`\n", indent, c.ParentID))
buf.WriteString(fmt.Sprintf("%s - Class: `%s`\n", indent, c.Class))
if c.Restricted != "" {
buf.WriteString(fmt.Sprintf("%s - Restricted: `%s`\n", indent, c.Restricted))
}
if c.ChildCount != "" {
buf.WriteString(fmt.Sprintf("%s - ChildCount: `%s`\n", indent, c.ChildCount))
}
// Sous-conteneurs
if len(c.Containers) > 0 {
buf.WriteString(fmt.Sprintf("%s - Subcontainers:\n", indent))
for _, sub := range c.Containers {
sub.markdown(buf, depth+2)
}
}
// Items du conteneur
if len(c.Items) > 0 {
buf.WriteString(fmt.Sprintf("%s - Items:\n", indent))
for _, item := range c.Items {
item.markdown(buf, depth+2)
}
}
buf.WriteString("\n")
}
func (i *Item) markdown(buf *strings.Builder, depth int) {
indent := strings.Repeat(" ", depth)
buf.WriteString(fmt.Sprintf("%s- **Item**: %s\n", indent, i.Title))
buf.WriteString(fmt.Sprintf("%s - ID: `%s`\n", indent, i.ID))
buf.WriteString(fmt.Sprintf("%s - ParentID: `%s`\n", indent, i.ParentID))
buf.WriteString(fmt.Sprintf("%s - Class: `%s`\n", indent, i.Class))
if i.Creator != "" {
buf.WriteString(fmt.Sprintf("%s - Creator: %s\n", indent, i.Creator))
}
if i.Artist != "" {
buf.WriteString(fmt.Sprintf("%s - Artist: %s\n", indent, i.Artist))
}
if i.Album != "" {
buf.WriteString(fmt.Sprintf("%s - Album: %s\n", indent, i.Album))
}
if i.Genre != "" {
buf.WriteString(fmt.Sprintf("%s - Genre: %s\n", indent, i.Genre))
}
if i.AlbumArt != "" {
buf.WriteString(fmt.Sprintf("%s - Album Art: ![Cover](%s)\n", indent, i.AlbumArt))
}
if i.Date != "" {
buf.WriteString(fmt.Sprintf("%s - Date: %s\n", indent, i.Date))
}
if i.OriginalTrackNumber != "" {
buf.WriteString(fmt.Sprintf("%s - Track: %s\n", indent, i.OriginalTrackNumber))
}
// Ressources
if len(i.Ress) > 0 {
buf.WriteString(fmt.Sprintf("%s - Resources:\n", indent))
for _, res := range i.Ress {
buf.WriteString(fmt.Sprintf("%s - URL: %s\n", indent, res.URL))
buf.WriteString(fmt.Sprintf("%s - Protocol: `%s`\n", indent, res.ProtocolInfo))
if res.Duration != "" {
buf.WriteString(fmt.Sprintf("%s - Duration: `%s`\n", indent, res.Duration))
}
if res.BitsPerSample != "" {
buf.WriteString(fmt.Sprintf("%s - BitsPerSample: `%s`\n", indent, res.BitsPerSample))
}
if res.SampleFrequency != "" {
buf.WriteString(fmt.Sprintf("%s - SampleFrequency: `%s`\n", indent, res.SampleFrequency))
}
if res.NrAudioChannels != "" {
buf.WriteString(fmt.Sprintf("%s - Channels: `%s`\n", indent, res.NrAudioChannels))
}
}
}
// Descriptions
if len(i.Descs) > 0 {
buf.WriteString(fmt.Sprintf("%s - Descriptions:\n", indent))
for _, desc := range i.Descs {
buf.WriteString(fmt.Sprintf("%s - Namespace: `%s`\n", indent, desc.NameSpace))
if desc.TrackGain != "" {
buf.WriteString(fmt.Sprintf("%s - Track Gain: `%s`\n", indent, desc.TrackGain))
}
if desc.TrackPeak != "" {
buf.WriteString(fmt.Sprintf("%s - Track Peak: `%s`\n", indent, desc.TrackPeak))
}
}
}
buf.WriteString("\n")
}

67
pmodidl/model.go Normal file
View File

@@ -0,0 +1,67 @@
package pmodidl
import "encoding/xml"
// DIDLLite représente la racine <DIDL-Lite>
type DIDLLite struct {
XMLName xml.Name `xml:"DIDL-Lite"`
Xmlns string `xml:"xmlns,attr"`
XmlnsUpnp string `xml:"xmlns:upnp,attr,omitempty"`
XmlnsDc string `xml:"xmlns:dc,attr,omitempty"`
XmlnsDlna string `xml:"xmlns:dlna,attr,omitempty"`
XmlnsSec string `xml:"xmlns:sec,attr,omitempty"`
XmlnsPv string `xml:"xmlns:pv,attr,omitempty"`
Containers []Container `xml:"container"`
Items []Item `xml:"item"`
}
// Container peut contenir d'autres containers ou des items audio
type Container struct {
ID string `xml:"id,attr"`
ParentID string `xml:"parentID,attr"`
Restricted string `xml:"restricted,attr,omitempty"`
ChildCount string `xml:"childCount,attr,omitempty"`
Title string `xml:"http://purl.org/dc/elements/1.1/ title"`
Class string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ class"`
Containers []Container `xml:"container"`
Items []Item `xml:"item"`
}
// Item représente un objet audio
type Item struct {
ID string `xml:"id,attr"`
ParentID string `xml:"parentID,attr"`
Restricted string `xml:"restricted,attr,omitempty"`
Title string `xml:"http://purl.org/dc/elements/1.1/ title"`
Creator string `xml:"http://purl.org/dc/elements/1.1/ creator,omitempty"`
Class string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ class"`
Artist string `xml:"urn:schemas-upnp-org:metadata-1-0/upnp/ artist,omitempty"`
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"`
Ress []Res `xml:"res"`
Descs []Desc `xml:"desc"`
}
// Res correspond aux fichiers média
type Res struct {
ProtocolInfo string `xml:"protocolInfo,attr"`
BitsPerSample string `xml:"bitsPerSample,attr,omitempty"`
SampleFrequency string `xml:"sampleFrequency,attr,omitempty"`
NrAudioChannels string `xml:"nrAudioChannels,attr,omitempty"`
Duration string `xml:"duration,attr,omitempty"`
URL string `xml:",chardata"`
}
// Desc correspond aux métadonnées optionnelles comme replaygain
type Desc struct {
ID string `xml:"id,attr,omitempty"`
NameSpace string `xml:"nameSpace,attr,omitempty"`
TrackGain string `xml:"track_gain,omitempty"`
TrackPeak string `xml:"track_peak,omitempty"`
}

26
pmodidl/parser.go Normal file
View File

@@ -0,0 +1,26 @@
package pmodidl
import (
"encoding/xml"
"fmt"
"gargoton.petite-maison-orange.fr/eric/pmomusic/pmocover"
)
func Parse(metadata string) (*DIDLLite, error) {
var didl DIDLLite
err := xml.Unmarshal([]byte(metadata), &didl)
if err != nil {
return nil, fmt.Errorf("failed to parse DIDL-Lite: %v", err)
}
cache, err := pmocover.GetCoverCache()
if err != nil {
return &didl, fmt.Errorf("failed to get cover cache: %v", err)
}
didl.CacheAllCoverArts(cache)
return &didl, nil
}