Skip to content

Commit dc2d778

Browse files
Merge pull request #46 from spezifisch/scrobble
Scrobbling Support
2 parents 3182c8d + d112c6c commit dc2d778

File tree

4 files changed

+68
-2
lines changed

4 files changed

+68
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ or the directory in which the executible is placed.
4747
[auth]
4848
username = 'admin'
4949
password = 'password'
50-
plaintext = true # Use 'legacy' unsalted password auth. (default: false)
50+
plaintext = true # Use 'legacy' unsalted password auth. (default: false)
5151

5252
[server]
5353
host = 'https://your-subsonic-host.tld'
54+
scrobble = true # Use Subsonic scrobbling for last.fm/ListenBrainz (default: false)
5455
```
5556

5657
## Usage

api.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type SubsonicConnection struct {
1919
Password string
2020
Host string
2121
PlaintextAuth bool
22+
Scrobble bool
2223
Logger Logger
2324
directoryCache map[string]SubsonicResponse
2425
}
@@ -214,6 +215,22 @@ func (connection *SubsonicConnection) GetRandomSongs() (*SubsonicResponse, error
214215
return resp, nil
215216
}
216217

218+
func (connection *SubsonicConnection) ScrobbleSubmission(id string, isSubmission bool) (*SubsonicResponse, error) {
219+
query := defaultQuery(connection)
220+
query.Set("id", id)
221+
222+
// optional field, false for "now playing", true for "submission"
223+
query.Set("submission", strconv.FormatBool(isSubmission))
224+
225+
requestUrl := connection.Host + "/rest/scrobble" + "?" + query.Encode()
226+
resp, err := connection.getResponse("ScrobbleSubmission", requestUrl)
227+
if err != nil {
228+
connection.Logger.Printf("ScrobbleSubmission error: %v", err)
229+
return resp, err
230+
}
231+
return resp, nil
232+
}
233+
217234
func (connection *SubsonicConnection) GetStarred() (*SubsonicResponse, error) {
218235
query := defaultQuery(connection)
219236
requestUrl := connection.Host + "/rest/getStarred" + "?" + query.Encode()

gui.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math"
66
"sort"
77
"strings"
8+
"time"
89

910
"github.com/gdamore/tcell/v2"
1011
"github.com/rivo/tview"
@@ -33,6 +34,7 @@ type Ui struct {
3334
playlists []SubsonicPlaylist
3435
connection *SubsonicConnection
3536
player *Player
37+
scrobbleTimer *time.Timer
3638
}
3739

3840
func (ui *Ui) handleEntitySelected(directoryId string) {
@@ -465,6 +467,12 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub
465467
// Stores the song IDs
466468
var starIdList = map[string]struct{}{}
467469

470+
// create reused timer to scrobble after delay
471+
scrobbleTimer := time.NewTimer(0)
472+
if !scrobbleTimer.Stop() {
473+
<-scrobbleTimer.C
474+
}
475+
468476
ui := Ui{
469477
app: app,
470478
pages: pages,
@@ -484,6 +492,7 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub
484492
playlists: *playlists,
485493
connection: connection,
486494
player: player,
495+
scrobbleTimer: scrobbleTimer,
487496
}
488497

489498
ui.addStarredToList()
@@ -499,6 +508,17 @@ func createUi(_ *[]SubsonicIndex, playlists *[]SubsonicPlaylist, connection *Sub
499508
ui.logList.RemoveItem(0)
500509
}
501510
})
511+
512+
case <-scrobbleTimer.C:
513+
// scrobble submission delay elapsed
514+
paused, err := ui.player.IsPaused()
515+
connection.Logger.Printf("scrobbler event: paused %v, err %v, qlen %d", paused, err, len(ui.player.Queue))
516+
isPlaying := err == nil && !paused
517+
if len(ui.player.Queue) > 0 && isPlaying {
518+
// it's still playing, submit it
519+
currentSong := ui.player.Queue[0]
520+
ui.connection.ScrobbleSubmission(currentSong.Id, true)
521+
}
502522
}
503523
}
504524
}()
@@ -917,8 +937,35 @@ func (ui *Ui) handleMpvEvents() {
917937
}
918938
} else if e.Event_Id == mpv.EVENT_START_FILE {
919939
ui.player.ReplaceInProgress = false
920-
ui.startStopStatus.SetText("[::b]stmp: [green]playing " + ui.player.Queue[0].Title)
921940
updateQueueList(ui.player, ui.queueList, ui.starIdList)
941+
942+
if len(ui.player.Queue) > 0 {
943+
currentSong := ui.player.Queue[0]
944+
ui.startStopStatus.SetText("[::b]stmp: [green]playing " + currentSong.Title)
945+
946+
if ui.connection.Scrobble {
947+
// scrobble "now playing" event
948+
ui.connection.ScrobbleSubmission(currentSong.Id, false)
949+
950+
// scrobble "submission" after song has been playing a bit
951+
// see: https://www.last.fm/api/scrobbling
952+
// A track should only be scrobbled when the following conditions have been met:
953+
// The track must be longer than 30 seconds. And the track has been played for
954+
// at least half its duration, or for 4 minutes (whichever occurs earlier.)
955+
if currentSong.Duration > 30 {
956+
scrobbleDelay := currentSong.Duration / 2
957+
if scrobbleDelay > 240 {
958+
scrobbleDelay = 240
959+
}
960+
scrobbleDuration := time.Duration(scrobbleDelay) * time.Second
961+
962+
ui.scrobbleTimer.Reset(scrobbleDuration)
963+
ui.connection.Logger.Printf("scrobbler: timer started, %v", scrobbleDuration)
964+
} else {
965+
ui.connection.Logger.Printf("scrobbler: track too short")
966+
}
967+
}
968+
}
922969
} else if e.Event_Id == mpv.EVENT_IDLE || e.Event_Id == mpv.EVENT_NONE {
923970
continue
924971
}

stmp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func main() {
5656
Password: viper.GetString("auth.password"),
5757
Host: viper.GetString("server.host"),
5858
PlaintextAuth: viper.GetBool("auth.plaintext"),
59+
Scrobble: viper.GetBool("server.scrobble"),
5960
Logger: logger,
6061
directoryCache: make(map[string]SubsonicResponse),
6162
}

0 commit comments

Comments
 (0)