append metadata when downloading episodes
This commit is contained in:
6
go.mod
6
go.mod
@@ -6,3 +6,9 @@ require (
|
|||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.34
|
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
6
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.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 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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
26
main.go
@@ -23,6 +23,7 @@ type Podcast struct {
|
|||||||
Feed string `json:"feed" db:"feed"`
|
Feed string `json:"feed" db:"feed"`
|
||||||
Language string `json:"language" db:"language"`
|
Language string `json:"language" db:"language"`
|
||||||
Link string `json:"link" db:"link"`
|
Link string `json:"link" db:"link"`
|
||||||
|
Image string `json:"image" db:"image"`
|
||||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ type Episode struct {
|
|||||||
Guid string `json:"guid" db:"guid"`
|
Guid string `json:"guid" db:"guid"`
|
||||||
Url string `json:"url" db:"url"`
|
Url string `json:"url" db:"url"`
|
||||||
PodcastId int64 `json:"podcastId" db:"podcast_id"`
|
PodcastId int64 `json:"podcastId" db:"podcast_id"`
|
||||||
|
Number int `json:"number" db:"number"`
|
||||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ type RSSFeed struct {
|
|||||||
Description string `xml:"description"`
|
Description string `xml:"description"`
|
||||||
Language string `xml:"language"`
|
Language string `xml:"language"`
|
||||||
Link string `xml:"link"`
|
Link string `xml:"link"`
|
||||||
|
Image string `xml:"image>url"`
|
||||||
Items []RSSFeedItem `xml:"item"`
|
Items []RSSFeedItem `xml:"item"`
|
||||||
} `xml:"channel"`
|
} `xml:"channel"`
|
||||||
}
|
}
|
||||||
@@ -126,12 +129,13 @@ func getPodcasts(db *sqlx.DB) ([]*Podcast, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createPodcast(db *sqlx.DB, feed RSSFeed, feedUrl string) 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,
|
"name": feed.Channel.Title,
|
||||||
"description": feed.Channel.Description,
|
"description": feed.Channel.Description,
|
||||||
"feed": feedUrl,
|
"feed": feedUrl,
|
||||||
"language": feed.Channel.Language,
|
"language": feed.Channel.Language,
|
||||||
"link": feed.Channel.Link,
|
"link": feed.Channel.Link,
|
||||||
|
"image": feed.Channel.Image,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,11 +154,12 @@ func getEpisodeByGuid(db *sqlx.DB, guid string) (*Episode, error) {
|
|||||||
return episode, nil
|
return episode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addEpisode(db *sqlx.DB, episode RSSFeedItem, podcastId int64) error {
|
func addEpisode(db *sqlx.DB, episode RSSFeedItem, episodeNumber int, 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{
|
_, 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,
|
"title": episode.Title,
|
||||||
"pubdate": episode.PubDate.Time,
|
"pubdate": episode.PubDate.Time,
|
||||||
"url": episode.Enclosure.URL,
|
"url": episode.Enclosure.URL,
|
||||||
|
"number": episodeNumber,
|
||||||
"guid": episode.Guid,
|
"guid": episode.Guid,
|
||||||
"podcast_id": podcastId,
|
"podcast_id": podcastId,
|
||||||
})
|
})
|
||||||
@@ -220,6 +225,7 @@ func main() {
|
|||||||
feed varchar not null,
|
feed varchar not null,
|
||||||
language varchar not null,
|
language varchar not null,
|
||||||
link varchar not null,
|
link varchar not null,
|
||||||
|
image varchar not null,
|
||||||
created_at datetime default current_timestamp
|
created_at datetime default current_timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,6 +235,7 @@ func main() {
|
|||||||
pubdate datetime not null,
|
pubdate datetime not null,
|
||||||
guid varchar not null,
|
guid varchar not null,
|
||||||
url varchar not null,
|
url varchar not null,
|
||||||
|
number integer not null,
|
||||||
created_at datetime default current_timestamp,
|
created_at datetime default current_timestamp,
|
||||||
podcast_id integer not null,
|
podcast_id integer not null,
|
||||||
FOREIGN KEY (podcast_id) REFERENCES podcasts(id)
|
FOREIGN KEY (podcast_id) REFERENCES podcasts(id)
|
||||||
@@ -258,12 +265,16 @@ func main() {
|
|||||||
continue
|
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)
|
log.Printf("failed to add new episode [%s]: %v\n", podcast.Name, err)
|
||||||
continue
|
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 {
|
if err != nil {
|
||||||
log.Printf("failed to create file for newest episode: %v\n", err)
|
log.Printf("failed to create file for newest episode: %v\n", err)
|
||||||
continue
|
continue
|
||||||
@@ -274,6 +285,11 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := appendPodcastMetadata(episodeFilePath, newestEpisode, episodeNumber, *podcast); err != nil {
|
||||||
|
log.Printf("failed to append episode metadata: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
metadata.go
Normal file
51
metadata.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user