Files
icecast-ripper/internal/rss/rss.go
Dmitrii Andreev f64163d12a Rework with go
2025-04-07 11:15:05 +03:00

143 lines
4.5 KiB
Go

package rss
import (
"encoding/xml"
"fmt"
"log/slog"
"net/url"
"time"
"github.com/kemko/icecast-ripper/internal/config"
"github.com/kemko/icecast-ripper/internal/database"
)
// Structs for RSS 2.0 feed generation
// Based on https://validator.w3.org/feed/docs/rss2.html
type RSS struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Channel Channel `xml:"channel"`
}
type Channel struct {
XMLName xml.Name `xml:"channel"`
Title string `xml:"title"`
Link string `xml:"link"` // Link to the website/source
Description string `xml:"description"`
LastBuildDate string `xml:"lastBuildDate,omitempty"` // RFC1123Z format
Items []Item `xml:"item"`
}
type Item struct {
XMLName xml.Name `xml:"item"`
Title string `xml:"title"`
Link string `xml:"link"` // Link to the specific recording file
Description string `xml:"description"` // Can include duration, size etc.
PubDate string `xml:"pubDate"` // RFC1123Z format of recording time
GUID GUID `xml:"guid"` // Unique identifier (using file hash)
Enclosure Enclosure `xml:"enclosure"` // Describes the media file
}
// GUID needs IsPermaLink attribute
type GUID struct {
XMLName xml.Name `xml:"guid"`
IsPermaLink bool `xml:"isPermaLink,attr"`
Value string `xml:",chardata"`
}
// Enclosure describes the media file
type Enclosure struct {
XMLName xml.Name `xml:"enclosure"`
URL string `xml:"url,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"` // MIME type (e.g., "audio/mpeg")
}
// Generator creates RSS feeds.
type Generator struct {
db *database.DB
feedBaseURL string // Base URL for links in the feed (e.g., http://server.com/recordings/)
recordingsPath string // Local path to recordings (needed for file info, maybe not directly used in feed)
feedTitle string
feedDesc string
}
// New creates a new RSS Generator instance.
func New(db *database.DB, cfg *config.Config, title, description string) *Generator {
// Ensure the base URL for recordings ends with a slash
baseURL := cfg.RSSFeedURL // This should be the URL base for *serving* files
if baseURL == "" {
slog.Warn("RSS_FEED_URL not set, RSS links might be incomplete. Using placeholder.")
baseURL = "http://localhost:8080/recordings/" // Placeholder
}
if baseURL[len(baseURL)-1:] != "/" {
baseURL += "/"
}
return &Generator{
db: db,
feedBaseURL: baseURL,
recordingsPath: cfg.RecordingsPath,
feedTitle: title,
feedDesc: description,
}
}
// GenerateFeed fetches recordings and produces the RSS feed XML as a byte slice.
func (g *Generator) GenerateFeed(maxItems int) ([]byte, error) {
slog.Debug("Generating RSS feed", "maxItems", maxItems)
recordings, err := g.db.GetRecordedFiles(maxItems)
if err != nil {
return nil, fmt.Errorf("failed to get recorded files for RSS feed: %w", err)
}
items := make([]Item, 0, len(recordings))
for _, rec := range recordings {
fileURL, err := url.JoinPath(g.feedBaseURL, rec.Filename)
if err != nil {
slog.Error("Failed to create file URL for RSS item", "filename", rec.Filename, "error", err)
continue // Skip this item if URL creation fails
}
item := Item{
Title: fmt.Sprintf("Recording %s", rec.RecordedAt.Format("2006-01-02 15:04")), // Example title
Link: fileURL,
Description: fmt.Sprintf("Icecast stream recording from %s. Duration: %s.", rec.RecordedAt.Format(time.RFC1123), rec.Duration.String()),
PubDate: rec.RecordedAt.Format(time.RFC1123Z), // Use RFC1123Z for pubDate
GUID: GUID{
IsPermaLink: false, // The hash itself is not a permalink URL
Value: rec.Hash,
},
Enclosure: Enclosure{
URL: fileURL,
Length: rec.FileSize,
Type: "audio/mpeg", // Assuming MP3, adjust if format varies or is detectable
},
}
items = append(items, item)
}
feed := RSS{
Version: "2.0",
Channel: Channel{
Title: g.feedTitle,
Link: g.feedBaseURL, // Link to the base URL or a relevant page
Description: g.feedDesc,
LastBuildDate: time.Now().Format(time.RFC1123Z),
Items: items,
},
}
xmlBytes, err := xml.MarshalIndent(feed, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal RSS feed to XML: %w", err)
}
// Add the standard XML header
output := append([]byte(xml.Header), xmlBytes...)
slog.Debug("RSS feed generated successfully", "item_count", len(items))
return output, nil
}