append metadata when downloading episodes

This commit is contained in:
2026-02-13 14:23:11 +03:00
parent 39d8286eb9
commit 7df08812bd
4 changed files with 84 additions and 5 deletions

6
go.mod
View File

@@ -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
)

6
go.sum
View File

@@ -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=

26
main.go
View File

@@ -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()
}
}

51
metadata.go Normal file
View File

@@ -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
}