From d678e047d6aaae1a8bcb26573a15dee2ead2e0c5 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 23 Oct 2023 10:49:00 +0200 Subject: [PATCH 1/3] add subsonic scrobble endpoint support --- api.go | 17 +++++++++++++++++ gui.go | 7 +++++++ stmp.go | 1 + 3 files changed, 25 insertions(+) diff --git a/api.go b/api.go index 234368f..fdd5a0f 100644 --- a/api.go +++ b/api.go @@ -19,6 +19,7 @@ type SubsonicConnection struct { Password string Host string PlaintextAuth bool + Scrobble bool Logger Logger directoryCache map[string]SubsonicResponse } @@ -214,6 +215,22 @@ func (connection *SubsonicConnection) GetRandomSongs() (*SubsonicResponse, error return resp, nil } +func (connection *SubsonicConnection) ScrobbleSubmission(id string, isSubmission bool) (*SubsonicResponse, error) { + query := defaultQuery(connection) + query.Set("id", id) + + // optional field, false for "now playing", true for "submission" + query.Set("submission", strconv.FormatBool(isSubmission)) + + requestUrl := connection.Host + "/rest/scrobble" + "?" + query.Encode() + resp, err := connection.getResponse("ScrobbleSubmission", requestUrl) + if err != nil { + connection.Logger.Printf("ScrobbleSubmission error: %v", err) + return resp, err + } + return resp, nil +} + func (connection *SubsonicConnection) GetStarred() (*SubsonicResponse, error) { query := defaultQuery(connection) requestUrl := connection.Host + "/rest/getStarred" + "?" + query.Encode() diff --git a/gui.go b/gui.go index 73c25e2..593e3e4 100644 --- a/gui.go +++ b/gui.go @@ -919,6 +919,13 @@ func (ui *Ui) handleMpvEvents() { ui.player.ReplaceInProgress = false ui.startStopStatus.SetText("[::b]stmp: [green]playing " + ui.player.Queue[0].Title) updateQueueList(ui.player, ui.queueList, ui.starIdList) + + if ui.connection.Scrobble { + // scrobble "now playing" event + ui.connection.ScrobbleSubmission(ui.player.Queue[0].Id, false) + // scrobble "submission" event + ui.connection.ScrobbleSubmission(ui.player.Queue[0].Id, true) + } } else if e.Event_Id == mpv.EVENT_IDLE || e.Event_Id == mpv.EVENT_NONE { continue } diff --git a/stmp.go b/stmp.go index 43f6cd1..b736cc0 100644 --- a/stmp.go +++ b/stmp.go @@ -56,6 +56,7 @@ func main() { Password: viper.GetString("auth.password"), Host: viper.GetString("server.host"), PlaintextAuth: viper.GetBool("auth.plaintext"), + Scrobble: viper.GetBool("server.scrobble"), Logger: logger, directoryCache: make(map[string]SubsonicResponse), } From 785504d0096f937782fad4f78a0fb74bce593984 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Mon, 23 Oct 2023 11:27:37 +0200 Subject: [PATCH 2/3] add scrobble config example --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 922dd0f..12cc8e6 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,11 @@ or the directory in which the executible is placed. [auth] username = 'admin' password = 'password' -plaintext = true # Use 'legacy' unsalted password auth. (default: false) +plaintext = true # Use 'legacy' unsalted password auth. (default: false) [server] host = 'https://your-subsonic-host.tld' +scrobble = true # Use Subsonic scrobbling for last.fm/ListenBrainz (default: false) ``` ## Usage From d112c6c6500046187500139081d7082da340de16 Mon Sep 17 00:00:00 2001 From: spezifisch Date: Tue, 24 Oct 2023 16:49:55 +0200 Subject: [PATCH 3/3] add scrobbling submission timer after audioscrobbler specs --- gui.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/gui.go b/gui.go index 593e3e4..5595ce4 100644 --- a/gui.go +++ b/gui.go @@ -5,6 +5,7 @@ import ( "math" "sort" "strings" + "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -33,6 +34,7 @@ type Ui struct { playlists []SubsonicPlaylist connection *SubsonicConnection player *Player + scrobbleTimer *time.Timer } func (ui *Ui) handleEntitySelected(directoryId string) { @@ -465,6 +467,12 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub // Stores the song IDs var starIdList = map[string]struct{}{} + // create reused timer to scrobble after delay + scrobbleTimer := time.NewTimer(0) + if !scrobbleTimer.Stop() { + <-scrobbleTimer.C + } + ui := Ui{ app: app, pages: pages, @@ -484,6 +492,7 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub playlists: *playlists, connection: connection, player: player, + scrobbleTimer: scrobbleTimer, } ui.addStarredToList() @@ -499,6 +508,17 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub ui.logList.RemoveItem(0) } }) + + case <-scrobbleTimer.C: + // scrobble submission delay elapsed + paused, err := ui.player.IsPaused() + connection.Logger.Printf("scrobbler event: paused %v, err %v, qlen %d", paused, err, len(ui.player.Queue)) + isPlaying := err == nil && !paused + if len(ui.player.Queue) > 0 && isPlaying { + // it's still playing, submit it + currentSong := ui.player.Queue[0] + ui.connection.ScrobbleSubmission(currentSong.Id, true) + } } } }() @@ -917,14 +937,34 @@ func (ui *Ui) handleMpvEvents() { } } else if e.Event_Id == mpv.EVENT_START_FILE { ui.player.ReplaceInProgress = false - ui.startStopStatus.SetText("[::b]stmp: [green]playing " + ui.player.Queue[0].Title) updateQueueList(ui.player, ui.queueList, ui.starIdList) - if ui.connection.Scrobble { - // scrobble "now playing" event - ui.connection.ScrobbleSubmission(ui.player.Queue[0].Id, false) - // scrobble "submission" event - ui.connection.ScrobbleSubmission(ui.player.Queue[0].Id, true) + if len(ui.player.Queue) > 0 { + currentSong := ui.player.Queue[0] + ui.startStopStatus.SetText("[::b]stmp: [green]playing " + currentSong.Title) + + if ui.connection.Scrobble { + // scrobble "now playing" event + ui.connection.ScrobbleSubmission(currentSong.Id, false) + + // scrobble "submission" after song has been playing a bit + // see: https://www.last.fm/api/scrobbling + // A track should only be scrobbled when the following conditions have been met: + // The track must be longer than 30 seconds. And the track has been played for + // at least half its duration, or for 4 minutes (whichever occurs earlier.) + if currentSong.Duration > 30 { + scrobbleDelay := currentSong.Duration / 2 + if scrobbleDelay > 240 { + scrobbleDelay = 240 + } + scrobbleDuration := time.Duration(scrobbleDelay) * time.Second + + ui.scrobbleTimer.Reset(scrobbleDuration) + ui.connection.Logger.Printf("scrobbler: timer started, %v", scrobbleDuration) + } else { + ui.connection.Logger.Printf("scrobbler: track too short") + } + } } } else if e.Event_Id == mpv.EVENT_IDLE || e.Event_Id == mpv.EVENT_NONE { continue