Skip to content

Commit

Permalink
improved artist metadata insertion
Browse files Browse the repository at this point in the history
  • Loading branch information
LumePart committed Oct 27, 2024
1 parent 58ff5a7 commit ef3169e
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 64 deletions.
2 changes: 2 additions & 0 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ YOUTUBE_API_KEY=
# LISTENBRAINZ_DISCOVERY=playlist
# Time to sleep (in minutes) between scanning and querying tracks from your system (If using Subsonic, Jellyfin)
# SLEEP=1
# How multiple artists are separated from eachother in metadata
# ARTIST_SEPARATOR=;
# Whether to provide additional info for debugging
# DEBUG=false
77 changes: 50 additions & 27 deletions src/listenbrainz.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ type Recommendations struct {
} `json:"payload"`
}

type Recording struct {
Length int `json:"length"`
Name string `json:"name"`
Rels []any `json:"rels"`
}

type Metadata struct {
Artist struct {
ArtistCreditID int `json:"artist_credit_id"`
Artists []struct {
ArtistMbid string `json:"artist_mbid"`
BeginYear int `json:"begin_year"`
EndYear int `json:"end_year,omitempty"`
JoinPhrase string `json:"join_phrase"`
Name string `json:"name"`
} `json:"artists"`
Name string `json:"name"`
} `json:"artist"`
Recording struct {
Length int `json:"length"`
Name string `json:"name"`
Expand Down Expand Up @@ -69,15 +74,32 @@ type Exploration struct {
Tracks []struct {
Album string `json:"album"`
Creator string `json:"creator"`
Extension struct {
HTTPSMusicbrainzOrgDocJspfTrack struct {
AddedAt time.Time `json:"added_at"`
AddedBy string `json:"added_by"`
AdditionalMetadata struct {
Artists []struct {
ArtistCreditName string `json:"artist_credit_name"`
ArtistMbid string `json:"artist_mbid"`
JoinPhrase string `json:"join_phrase"`
} `json:"artists"`
CaaID int64 `json:"caa_id"`
CaaReleaseMbid string `json:"caa_release_mbid"`
} `json:"additional_metadata"`
ArtistIdentifiers []string `json:"artist_identifiers"`
} `json:"https://musicbrainz.org/doc/jspf#track"`
} `json:"extension"`
Identifier []string `json:"identifier"`
Title string `json:"title"`
} `json:"track"`
} `json:"playlist"`
}

type Track []struct {
type Track struct {
Album string
Artist string
SearchArtist string // used for searching in youtube
MetadataArtist string
Title string
}

Expand Down Expand Up @@ -106,8 +128,8 @@ func getReccs(cfg Listenbrainz) []string {
return mbids
}

func getTracks(mbids []string) Track {
var tracks Track
func getTracks(mbids []string, artistSeparator string) []Track {
var tracks []Track
var recordings Recordings
str_mbids := strings.Join(mbids, ",")

Expand All @@ -122,13 +144,15 @@ func getTracks(mbids []string) Track {
log.Fatalf("failed to unmarshal body: %s", err.Error())
}
for _, recording := range recordings {
tracks = append(tracks, struct {
Album string
Artist string
Title string
}{
var metadataArtists []string
for _, artist := range recording.Artist.Artists {
metadataArtists = append(metadataArtists, artist.Name)
}

tracks = append(tracks, Track{
Album: recording.Release.Name,
Artist: recording.Release.AlbumArtistName,
SearchArtist: recording.Release.AlbumArtistName,
MetadataArtist: strings.Join(metadataArtists, artistSeparator),
Title: recording.Recording.Name,
})
}
Expand All @@ -138,7 +162,6 @@ func getTracks(mbids []string) Track {
}

func getWeeklyExploration(cfg Listenbrainz) (string, error) {

var playlists Playlists

body, err := lbRequest(fmt.Sprintf("user/%s/playlists/createdfor", cfg.User))
Expand All @@ -165,10 +188,8 @@ func getWeeklyExploration(cfg Listenbrainz) (string, error) {
return "", fmt.Errorf("failed to get new exploration playlist, check if ListenBrainz has generated one this week")
}

func parseWeeklyExploration(identifier string) Track {

var tracks Track

func parseWeeklyExploration(identifier, artistSeparator string) []Track {
var tracks []Track
var exploration Exploration

body, err := lbRequest(fmt.Sprintf("playlist/%s", identifier))
Expand All @@ -183,13 +204,15 @@ func parseWeeklyExploration(identifier string) Track {
}

for _, track := range exploration.Playlist.Tracks {
tracks = append(tracks, struct {
Album string
Artist string
Title string
}{
var metadataArtists []string
for _, artist := range track.Extension.HTTPSMusicbrainzOrgDocJspfTrack.AdditionalMetadata.Artists {
metadataArtists = append(metadataArtists, artist.ArtistCreditName)
}

tracks = append(tracks, Track{
Album: track.Album,
Artist: track.Creator,
SearchArtist: track.Creator,
MetadataArtist: strings.Join(metadataArtists, artistSeparator),
Title: track.Title,
})
}
Expand Down
17 changes: 8 additions & 9 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Youtube struct {
type Listenbrainz struct {
Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"`
User string `env:"LISTENBRAINZ_USER"`
ArtistSeparator string `env:"ARTIST_SEPARARTOR" env-default:";"`
}

type Song struct {
Expand Down Expand Up @@ -236,17 +237,17 @@ func main() {
cfg.getPlaylistName()
debug.Init(cfg.Debug)

var tracks Track
var tracks []Track

if cfg.Listenbrainz.Discovery == "playlist" {
id, err := getWeeklyExploration(cfg.Listenbrainz)
if err != nil {
log.Fatal(err.Error())
}
tracks = parseWeeklyExploration(id)
tracks = parseWeeklyExploration(id, cfg.Listenbrainz.ArtistSeparator)
} else {
mbids := getReccs(cfg.Listenbrainz)
tracks = getTracks(mbids)
tracks = getTracks(mbids, cfg.Listenbrainz.ArtistSeparator)
}

if !cfg.Persist { // delete songs and playlist before downloading new ones
Expand All @@ -256,25 +257,23 @@ func main() {
}

var files []string
var songs []Song
var m3usongs []string

for _, track := range tracks {
song, file := gatherVideo(cfg.Youtube, track.Title, track.Artist, track.Album)
file := gatherVideo(cfg.Youtube, track)
files = append(files, file) // used for deleting .webms
if (song != Song{}) { // used for creating playlists
if (track != Track{}) { // used for creating playlists
m3usongs = append(m3usongs, file)
songs = append(songs, song)
}
if cfg.Listenbrainz.Discovery == "test" && (song != Song{}) {
if cfg.Listenbrainz.Discovery == "test" && (track != Track{}) {
log.Println("using 'test' discovery method. Downloaded 1 song.")
break
}
}

cleanUp(cfg, files)

err := createPlaylist(cfg, songs, m3usongs)
err := createPlaylist(cfg, tracks, m3usongs)
if err != nil {
log.Fatal(err.Error())
}
Expand Down
4 changes: 2 additions & 2 deletions src/playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (cfg *Config) getPlaylistName() {
cfg.PlaylistName = playlistName
}

func createPlaylist(cfg Config, songs []Song, files []string) error {
func createPlaylist(cfg Config, tracks []Track, files []string) error {

if cfg.System == "" {
return fmt.Errorf("could not get music system")
Expand All @@ -49,7 +49,7 @@ func createPlaylist(cfg Config, songs []Song, files []string) error {
log.Printf("sleeping for %d minutes, to allow scan to complete..", cfg.Sleep)
time.Sleep(time.Duration(cfg.Sleep) * time.Minute)

if err := subsonicPlaylist(cfg, songs); err != nil {
if err := subsonicPlaylist(cfg, tracks); err != nil {
return fmt.Errorf("failed to create subsonic playlist: %s", err.Error())
}
return nil
Expand Down
6 changes: 3 additions & 3 deletions src/subsonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,13 @@ func searchTrack(cfg Config, track string) (string, error) {
return resp.SubsonicResponse.SearchResult3.Song[0].ID, nil
}

func subsonicPlaylist(cfg Config, songs []Song) error {
func subsonicPlaylist(cfg Config, tracks []Track) error {

var trackIDs string
var reqParam string

for _, song := range songs { // Get track IDs from app and format them
ID, err := searchTrack(cfg, fmt.Sprintf("%s %s %s", song.Title, song.Artist, song.Album))
for _, song := range tracks { // Get track IDs from app and format them
ID, err := searchTrack(cfg, fmt.Sprintf("%s %s %s", song.Title, song.MetadataArtist, song.Album))
if ID == "" || err != nil { // if ID is empty, skip song
continue
}
Expand Down
46 changes: 23 additions & 23 deletions src/youtube.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ type Item struct {



func queryYT(cfg Youtube, song, artist string) Videos { // Queries youtube for the song
func queryYT(cfg Youtube, track Track) Videos { // Queries youtube for the song

escQuery := url.PathEscape(fmt.Sprintf("%s - %s", song, artist))
escQuery := url.PathEscape(fmt.Sprintf("%s - %s", track.Title, track.SearchArtist))
queryURL := fmt.Sprintf("https://youtube.googleapis.com/youtube/v3/search?part=snippet&q=%s&type=video&videoCategoryId=10&key=%s", escQuery, cfg.APIKey)

body, err := makeRequest("GET", queryURL, nil, nil)
Expand All @@ -56,10 +56,10 @@ func queryYT(cfg Youtube, song, artist string) Videos { // Queries youtube for t

}

func getTopic(videos Videos, song, artist string) string { // gets song under artist topic or personal channel
func getTopic(videos Videos, track Track) string { // gets song under artist topic or personal channel

for _, v := range videos.Items {
if (strings.Contains(v.Snippet.ChannelTitle, "- Topic") || v.Snippet.ChannelTitle == artist) && filter(song, artist, v.Snippet.Title) {
if (strings.Contains(v.Snippet.ChannelTitle, "- Topic") || v.Snippet.ChannelTitle == track.MetadataArtist) && filter(track, v.Snippet.Title) {
return v.ID.VideoID
} else {
continue
Expand Down Expand Up @@ -99,13 +99,13 @@ func getVideo(videoID string) (io.ReadCloser, error) { // gets video stream usin

}

func saveVideo(cfg Youtube, song, artist, album string, stream io.ReadCloser) (Song, string) {
func saveVideo(cfg Youtube, track Track, stream io.ReadCloser) string {

defer stream.Close()
// Remove illegal characters for file naming
re := regexp.MustCompile("[^a-zA-Z0-9._]+")
s := re.ReplaceAllString(song, cfg.Separator)
a := re.ReplaceAllString(artist, cfg.Separator)
s := re.ReplaceAllString(track.Title, cfg.Separator)
a := re.ReplaceAllString(track.SearchArtist, cfg.Separator)

input := fmt.Sprintf("%s%s-%s.webm", cfg.DownloadDir,s, a)
file, err := os.Create(input)
Expand All @@ -117,12 +117,12 @@ func saveVideo(cfg Youtube, song, artist, album string, stream io.ReadCloser) (S
_, err = io.Copy(file, stream)
if err != nil {
log.Printf("Failed to copy stream to file: %s", err.Error())
return Song{}, fmt.Sprintf("%s-%s", s, a) // If the download fails (downloads a few bytes) then it will get triggered here: "tls: bad record MAC"
return fmt.Sprintf("%s-%s", s, a) // If the download fails (downloads a few bytes) then it will get triggered here: "tls: bad record MAC"
}

cmd := ffmpeg.Input(input).Output(fmt.Sprintf("%s%s-%s.mp3", cfg.DownloadDir,s, a), ffmpeg.KwArgs{
"map": "0:a",
"metadata": []string{"artist="+artist,"title="+song,"album="+album},
"metadata": []string{"artist="+track.MetadataArtist,"title="+track.Title,"album="+track.Album},
"loglevel": "error",
}).OverWriteOutput().ErrorToStdOut()

Expand All @@ -133,49 +133,49 @@ func saveVideo(cfg Youtube, song, artist, album string, stream io.ReadCloser) (S
err = cmd.Run()
if err != nil {
log.Printf("Failed to convert audio: %s", err.Error())
return Song{}, fmt.Sprintf("%s-%s", s, a)
return fmt.Sprintf("%s-%s", s, a)
}
return Song{Title: song, Artist: artist, Album: album}, fmt.Sprintf("%s-%s", s, a)
return fmt.Sprintf("%s-%s", s, a)

}

func gatherVideo(cfg Youtube, song, artist, album string) (Song, string) {
func gatherVideo(cfg Youtube, track Track) string {

videos := queryYT(cfg, song, artist)
id := getTopic(videos, song, artist)
videos := queryYT(cfg, track)
id := getTopic(videos, track)

if id != "" {
stream, err := getVideo(id)
if stream != nil && err == nil {
song, file := saveVideo(cfg, song, artist, album, stream)
return song, file
file := saveVideo(cfg, track, stream)
return file
} else {
log.Printf("failed getting stream: %s", err.Error())
}
}
// if getting song from official channel fails, try getting from first available channel
for _, video := range videos.Items {
if filter(song, artist, video.Snippet.Title) {
if filter(track, video.Snippet.Title) {
stream, err := getVideo(video.ID.VideoID)
if stream != nil && err == nil {
song, file := saveVideo(cfg, song, artist, album, stream)
return song, file
file := saveVideo(cfg, track, stream)
return file
} else {
log.Printf("failed getting stream: %s", err.Error())
continue
}
}
}
return Song{}, ""
return ""
}

func filter(song, artist, videoTitle string) bool { // ignore artist lives or song remixes
func filter(track Track, videoTitle string) bool { // ignore artist lives or song remixes

if (!contains(song,"live") && !contains(artist,"live") && contains(videoTitle, "live")) {
if (!contains(track.Title,"live") && !contains(track.MetadataArtist,"live") && contains(videoTitle, "live")) {
return false
}

if (!contains(song,"remix") && !contains(artist,"remix") && contains(videoTitle, "remix")) {
if (!contains(track.Title,"remix") && !contains(track.MetadataArtist,"remix") && contains(videoTitle, "remix")) {
return false
}

Expand Down

0 comments on commit ef3169e

Please sign in to comment.