Maintenant capter les URL des documents didl parser et les stocker dans le cache...
This commit is contained in:
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
|
||||
}
|
||||
129
pmodidl/markdown.go
Normal file
129
pmodidl/markdown.go
Normal 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: \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
67
pmodidl/model.go
Normal 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
26
pmodidl/parser.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user