diff --git a/cmd/pmomusic/main.go b/cmd/pmomusic/main.go index 7bcf57cb..f19c0b9e 100644 --- a/cmd/pmomusic/main.go +++ b/cmd/pmomusic/main.go @@ -4,6 +4,7 @@ import ( "context" "os/signal" "syscall" + "time" log "github.com/sirupsen/logrus" @@ -28,4 +29,7 @@ func main() { if err := server.Run(ctx); err != nil { log.Fatalf("server error: %v", err) } + + time.Sleep(2 * time.Second) + } diff --git a/didl/markdown.go b/didl/markdown.go new file mode 100644 index 00000000..b424be5d --- /dev/null +++ b/didl/markdown.go @@ -0,0 +1,129 @@ +package didl + +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") +} diff --git a/didl/model.go b/didl/model.go new file mode 100644 index 00000000..5dad7d49 --- /dev/null +++ b/didl/model.go @@ -0,0 +1,66 @@ +package didl + +import "encoding/xml" + +// DIDLLite représente la racine +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"` + 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"` +} diff --git a/didl/parser.go b/didl/parser.go new file mode 100644 index 00000000..08297f87 --- /dev/null +++ b/didl/parser.go @@ -0,0 +1,17 @@ +package didl + +import ( + "encoding/xml" + "fmt" +) + +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) + + } + + return &didl, nil +} diff --git a/pmolog/webloger.go b/pmolog/webloger.go index 97f5dec6..edccc5b5 100644 --- a/pmolog/webloger.go +++ b/pmolog/webloger.go @@ -27,13 +27,12 @@ func (hook *SSELogHook) Levels() []logrus.Level { } func (hook *SSELogHook) Fire(entry *logrus.Entry) error { - // Formater le log + // Formater le log avec le niveau et le message logLine := fmt.Sprintf("[%s] %s", entry.Level.String(), entry.Message) // Envoyer le log à tous les clients connectés broker.mutex.Lock() for client := range broker.clients { - // Non-bloquant pour éviter qu'un client lent ne bloque tout select { case client <- logLine: default: @@ -62,14 +61,29 @@ func sseHandler(w http.ResponseWriter, r *http.Request) { broker.mutex.Unlock() // Envoyer un message de bienvenue - fmt.Fprintf(w, "data: %s\n\n", "Connexion établie. Attente des logs...") + fmt.Fprintf(w, "event: message\ndata: %s\n\n", "{\"content\": \"Connexion établie. Attente des logs...\", \"level\": \"info\"}") w.(http.Flusher).Flush() // Envoyer les logs au client au fur et à mesure for { select { case msg := <-messageChan: - fmt.Fprintf(w, "data: %s\n\n", msg) + // Déterminer le niveau de log pour le style CSS + level := "info" + if len(msg) > 7 { + switch msg[1:6] { + case "ERROR": + level = "error" + case "WARNI": + level = "warning" + case "DEBUG": + level = "debug" + } + } + + // Formater le message en JSON pour inclure le niveau + jsonMsg := fmt.Sprintf("{\"content\": \"%s\", \"level\": \"%s\"}", escapeJSONString(msg), level) + fmt.Fprintf(w, "event: message\ndata: %s\n\n", jsonMsg) w.(http.Flusher).Flush() case <-r.Context().Done(): // Supprimer le client quand la connexion est fermée @@ -82,43 +96,168 @@ func sseHandler(w http.ResponseWriter, r *http.Request) { } } -// Page HTML pour afficher les logs +// Fonction pour échapper les chaînes JSON +func escapeJSONString(s string) string { + // Échapper les guillemets et les antislashes + escaped := "" + for _, c := range s { + switch c { + case '"': + escaped += "\\\"" + case '\\': + escaped += "\\\\" + case '\n': + escaped += "\\n" + case '\r': + escaped += "\\r" + case '\t': + escaped += "\\t" + default: + escaped += string(c) + } + } + return escaped +} + +// Page HTML pour afficher les logs avec support Markdown var indexHTML = ` Logs en temps réel + + -

Logs en temps réel

-
+
+

📝 Logs en temps réel

+
+