From 7df08812bd04327127b19d16fde9c7d441ac5080 Mon Sep 17 00:00:00 2001 From: Daniil Tsivinsky Date: Fri, 13 Feb 2026 14:23:11 +0300 Subject: [PATCH] append metadata when downloading episodes --- go.mod | 6 ++++++ go.sum | 6 ++++++ main.go | 26 +++++++++++++++++++++----- metadata.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 metadata.go diff --git a/go.mod b/go.mod index 18e5e86..5bdbd52 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,9 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/mattn/go-sqlite3 v1.14.34 ) + +require ( + github.com/tetratelabs/wazero v1.11.0 // indirect + go.senan.xyz/taglib v0.11.1 // indirect + golang.org/x/sys v0.41.0 // indirect +) diff --git a/go.sum b/go.sum index 53de804..3e7744b 100644 --- a/go.sum +++ b/go.sum @@ -9,3 +9,9 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +go.senan.xyz/taglib v0.11.1 h1:S3mO5e3HRRG0Ehw1jLUodYbAJK8TtqdOoNgqkC0D3uU= +go.senan.xyz/taglib v0.11.1/go.mod h1:qyTl978MnGeZ/ny4d/t0ErLXxysA+39X4+SNSCk56Zs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/main.go b/main.go index 044119b..8a8f925 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ type Podcast struct { Feed string `json:"feed" db:"feed"` Language string `json:"language" db:"language"` Link string `json:"link" db:"link"` + Image string `json:"image" db:"image"` CreatedAt time.Time `json:"createdAt" db:"created_at"` } @@ -33,6 +34,7 @@ type Episode struct { Guid string `json:"guid" db:"guid"` Url string `json:"url" db:"url"` PodcastId int64 `json:"podcastId" db:"podcast_id"` + Number int `json:"number" db:"number"` CreatedAt time.Time `json:"createdAt" db:"created_at"` } @@ -72,6 +74,7 @@ type RSSFeed struct { Description string `xml:"description"` Language string `xml:"language"` Link string `xml:"link"` + Image string `xml:"image>url"` Items []RSSFeedItem `xml:"item"` } `xml:"channel"` } @@ -126,12 +129,13 @@ func getPodcasts(db *sqlx.DB) ([]*Podcast, error) { } func createPodcast(db *sqlx.DB, feed RSSFeed, feedUrl string) error { - _, err := db.NamedExec("INSERT INTO podcasts (name, description, feed, language, link) VALUES (:name, :description, :feed, :language, :link)", map[string]any{ + _, err := db.NamedExec("INSERT INTO podcasts (name, description, feed, language, link, image) VALUES (:name, :description, :feed, :language, :link, :image)", map[string]any{ "name": feed.Channel.Title, "description": feed.Channel.Description, "feed": feedUrl, "language": feed.Channel.Language, "link": feed.Channel.Link, + "image": feed.Channel.Image, }) return err } @@ -150,11 +154,12 @@ func getEpisodeByGuid(db *sqlx.DB, guid string) (*Episode, error) { return episode, nil } -func addEpisode(db *sqlx.DB, episode RSSFeedItem, podcastId int64) error { - _, err := db.NamedExec("INSERT INTO episodes (title, pubdate, guid, url, podcast_id) VALUES (:title, :pubdate, :guid, :url, :podcast_id)", map[string]any{ +func addEpisode(db *sqlx.DB, episode RSSFeedItem, episodeNumber int, podcastId int64) error { + _, err := db.NamedExec("INSERT INTO episodes (title, pubdate, guid, url, number, podcast_id) VALUES (:title, :pubdate, :guid, :url, :number, :podcast_id)", map[string]any{ "title": episode.Title, "pubdate": episode.PubDate.Time, "url": episode.Enclosure.URL, + "number": episodeNumber, "guid": episode.Guid, "podcast_id": podcastId, }) @@ -220,6 +225,7 @@ func main() { feed varchar not null, language varchar not null, link varchar not null, + image varchar not null, created_at datetime default current_timestamp ); @@ -229,6 +235,7 @@ func main() { pubdate datetime not null, guid varchar not null, url varchar not null, + number integer not null, created_at datetime default current_timestamp, podcast_id integer not null, FOREIGN KEY (podcast_id) REFERENCES podcasts(id) @@ -258,12 +265,16 @@ func main() { continue } - if err := addEpisode(db, newestEpisode, podcast.ID); err != nil { + episodeNumber := len(feed.Channel.Items) - 1 + + if err := addEpisode(db, newestEpisode, episodeNumber, podcast.ID); err != nil { log.Printf("failed to add new episode [%s]: %v\n", podcast.Name, err) continue } - f, err := os.OpenFile(path.Join(podcastsDirPath, podcast.Name, newestEpisode.Title+".mp3"), os.O_CREATE|os.O_RDWR, 0644) + episodeFilePath := path.Join(podcastsDirPath, podcast.Name, fmt.Sprintf("Episode %d.mp3", episodeNumber)) + + f, err := os.OpenFile(episodeFilePath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { log.Printf("failed to create file for newest episode: %v\n", err) continue @@ -274,6 +285,11 @@ func main() { continue } + if err := appendPodcastMetadata(episodeFilePath, newestEpisode, episodeNumber, *podcast); err != nil { + log.Printf("failed to append episode metadata: %v\n", err) + continue + } + f.Close() } } diff --git a/metadata.go b/metadata.go new file mode 100644 index 0000000..5cdfc5f --- /dev/null +++ b/metadata.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strconv" + + "go.senan.xyz/taglib" +) + +func getPodcastCover(imageUrl string) ([]byte, error) { + resp, err := http.Get(imageUrl) + if err != nil { + return nil, fmt.Errorf("failed to fetch podcast cover image: %v", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read image data from response: %v", err) + } + + return data, nil +} + +func appendPodcastMetadata(filePath string, feedItem RSSFeedItem, episodeNumber int, podcast Podcast) error { + if err := taglib.WriteTags(filePath, map[string][]string{ + taglib.Artist: {podcast.Name}, + taglib.AlbumArtist: {podcast.Name}, + taglib.Album: {podcast.Name}, + taglib.TrackNumber: {strconv.Itoa(episodeNumber)}, + taglib.Podcast: {podcast.Name}, + taglib.PodcastURL: {podcast.Link}, + taglib.PodcastDesc: {podcast.Description}, + taglib.Title: {feedItem.Title}, + }, 0); err != nil { + return fmt.Errorf("failed to append tags: %v", err) + } + + imgData, err := getPodcastCover(podcast.Image) + if err != nil { + return fmt.Errorf("failed to fetch podcast cover: %v", err) + } + + if err := taglib.WriteImage(filePath, imgData); err != nil { + return fmt.Errorf("failed to append cover image: %v", err) + } + + return nil +}