tangled
alpha
login
or
join now
tom.sherman.is
/
piper
0
fork
atom
A fork of https://github.com/teal-fm/piper
0
fork
atom
overview
issues
pulls
pipelines
lfm: handle now playing track separately
Natalie B
11 months ago
45e05bef
9d2d675c
+89
-44
2 changed files
expand all
collapse all
unified
split
models
atproto.go
service
lastfm
lastfm.go
+15
models/atproto.go
···
1
package models
2
3
import (
0
0
4
"github.com/lestrrat-go/jwx/v2/jwk"
5
)
6
···
14
DPoPAuthServerNonce string `json:"dpop_authserver_nonce"`
15
DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"`
16
}
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
package models
2
3
import (
4
+
"time"
5
+
6
"github.com/lestrrat-go/jwx/v2/jwk"
7
)
8
···
16
DPoPAuthServerNonce string `json:"dpop_authserver_nonce"`
17
DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"`
18
}
19
+
20
+
type TealFmPlayLexicon struct {
21
+
Type string `json:"$type"`
22
+
Duration int `json:"duration"`
23
+
TrackName string `json:"trackName"`
24
+
PlayedTime time.Time `json:"playedTime"`
25
+
ArtistMbIDs []string `json:"artistMbIds"`
26
+
ArtistNames []string `json:"artistNames"`
27
+
ReleaseMbID string `json:"releaseMbId"`
28
+
ReleaseName string `json:"releaseName"`
29
+
RecordingMbID string `json:"recordingMbId"`
30
+
SubmissionClientAgent string `json:"submissionClientAgent"`
31
+
}
+74
-44
service/lastfm/lastfm.go
···
42
Name string `json:"name"`
43
URL string `json:"url"`
44
Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields
45
-
NowPlaying *struct { // Custom handling for @attr.nowplaying
46
NowPlaying string `json:"nowplaying"` // Field name corrected to match struct tag
47
} `json:"@attr,omitempty"` // This captures the @attr object within the track
48
}
···
82
apiKey string
83
Usernames []string
84
musicBrainzService *musicbrainz.MusicBrainzService
85
-
// Removed in-memory map, assuming DB handles last seen state
86
-
// lastSeenTrackDate map[string]time.Time
87
-
// mu sync.Mutex // Keep mutex if other shared state is added later
88
}
89
90
func NewLastFMService(db *db.DB, apiKey string, musicBrainzService *musicbrainz.MusicBrainzService) *LastFMService {
···
99
Usernames: make([]string, 0),
100
// lastSeenTrackDate: make(map[string]time.Time), // Removed
101
musicBrainzService: musicBrainzService,
0
0
102
}
103
}
104
···
302
}
303
}
304
305
-
// processTracks processes the fetched tracks for a user, adding new scrobbles to the DB.
306
func (l *LastFMService) processTracks(username string, tracks []Track) error {
307
if l.db == nil {
308
return fmt.Errorf("database connection is nil")
309
}
310
311
-
// get uid
312
user, err := l.db.GetUserByLastFM(username)
313
if err != nil {
314
return fmt.Errorf("failed to get user ID for %s: %w", username, err)
315
}
316
317
-
lastKnownTimestamp, err := l.db.GetLastScrobbleTimestamp(user.ID) // Hypothetical DB call
318
if err != nil {
319
return fmt.Errorf("failed to get last scrobble timestamp for %s: %w", username, err)
320
}
321
322
found := lastKnownTimestamp == nil
323
if found {
324
-
log.Printf("No previous scrobble timestamp found for user %s. Processing latest track.", username)
325
} else {
326
-
log.Printf("Last known scrobble for %s was at %s", username, lastKnownTimestamp.Format(time.RFC3339))
327
}
328
329
-
processedCount := 0
330
-
var latestProcessedTime time.Time
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
331
0
0
332
for i := len(tracks) - 1; i >= 0; i-- {
333
-
track := tracks[i]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
334
335
-
// skip now playing
336
-
if track.NowPlaying != nil && track.NowPlaying.NowPlaying == "true" {
337
-
log.Printf("Skipping 'now playing' track for %s: %s - %s", username, track.Artist.Text, track.Name)
338
-
continue
339
}
340
341
-
// skip tracks w/out valid date (should be none, but just in case)
342
if track.Date == nil || track.Date.UTS == "" {
343
-
log.Printf("Skipping track without timestamp for %s: %s - %s", username, track.Artist.Text, track.Name)
344
continue
345
}
346
347
-
// parse uts (unix timestamp string)
348
uts, err := strconv.ParseInt(track.Date.UTS, 10, 64)
349
if err != nil {
350
-
log.Printf("Error parsing timestamp '%s' for track %s - %s: %v", track.Date.UTS, track.Artist.Text, track.Name, err)
351
continue
352
}
353
trackTime := time.Unix(uts, 0)
354
355
-
if lastKnownTimestamp != nil && !trackTime.After(*lastKnownTimestamp) {
356
if processedCount == 0 {
357
-
log.Printf("Reached already known scrobbles for user %s (Track time: %s, Last known: %s).",
358
-
username,
359
-
trackTime.Format(time.RFC3339),
360
-
lastKnownTimestamp.UTC().Format(time.RFC3339))
361
}
362
break
363
}
364
365
-
unhydratedArtist := []models.Artist{
366
-
{
367
-
Name: track.Artist.Text,
368
-
MBID: track.Artist.MBID,
369
-
},
370
-
}
371
-
372
-
mTrack := models.Track{
373
Name: track.Name,
374
URL: track.URL,
375
ServiceBaseUrl: "last.fm",
376
Album: track.Album.Text,
377
-
Timestamp: time.Unix(uts, 0),
378
-
Artist: unhydratedArtist,
0
0
0
0
0
379
}
380
381
-
// Fix based on diagnostic: Assume HydrateTrack returns (*models.Track, error)
382
-
hydratedTrackPtr, err := musicbrainz.HydrateTrack(l.musicBrainzService, mTrack)
383
if err != nil {
384
-
// Log hydration error specifically
385
-
log.Printf("Error hydrating track details for user %s, track %s - %s: %v", username, track.Artist.Text, track.Name, err)
386
-
// fallback to original track if hydration fails
387
-
hydratedTrackPtr = &mTrack
388
continue
389
}
390
391
-
l.db.SaveTrack(user.ID, hydratedTrackPtr)
392
-
393
processedCount++
394
395
if trackTime.After(latestProcessedTime) {
···
402
}
403
404
if processedCount > 0 {
405
-
log.Printf("Successfully processed %d new track(s) for user %s. Latest timestamp in batch: %s",
406
processedCount, username, latestProcessedTime.Format(time.RFC3339))
407
}
408
···
42
Name string `json:"name"`
43
URL string `json:"url"`
44
Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields
45
+
Attr *struct { // Custom handling for @attr.nowplaying
46
NowPlaying string `json:"nowplaying"` // Field name corrected to match struct tag
47
} `json:"@attr,omitempty"` // This captures the @attr object within the track
48
}
···
82
apiKey string
83
Usernames []string
84
musicBrainzService *musicbrainz.MusicBrainzService
85
+
lastSeenNowPlaying map[string]Track
86
+
mu sync.Mutex
0
87
}
88
89
func NewLastFMService(db *db.DB, apiKey string, musicBrainzService *musicbrainz.MusicBrainzService) *LastFMService {
···
98
Usernames: make([]string, 0),
99
// lastSeenTrackDate: make(map[string]time.Time), // Removed
100
musicBrainzService: musicBrainzService,
101
+
lastSeenNowPlaying: make(map[string]Track),
102
+
mu: sync.Mutex{},
103
}
104
}
105
···
303
}
304
}
305
0
306
func (l *LastFMService) processTracks(username string, tracks []Track) error {
307
if l.db == nil {
308
return fmt.Errorf("database connection is nil")
309
}
310
0
311
user, err := l.db.GetUserByLastFM(username)
312
if err != nil {
313
return fmt.Errorf("failed to get user ID for %s: %w", username, err)
314
}
315
316
+
lastKnownTimestamp, err := l.db.GetLastScrobbleTimestamp(user.ID)
317
if err != nil {
318
return fmt.Errorf("failed to get last scrobble timestamp for %s: %w", username, err)
319
}
320
321
found := lastKnownTimestamp == nil
322
if found {
323
+
log.Printf("no previous scrobble timestamp found for user %s. processing latest track.", username)
324
} else {
325
+
log.Printf("last known scrobble for %s was at %s", username, lastKnownTimestamp.Format(time.RFC3339))
326
}
327
328
+
var (
329
+
processedCount int
330
+
latestProcessedTime time.Time
331
+
)
332
+
333
+
// handle now playing track separately
334
+
if len(tracks) > 0 && tracks[0].Attr != nil && tracks[0].Attr.NowPlaying == "true" {
335
+
nowPlayingTrack := tracks[0]
336
+
log.Printf("now playing track for %s: %s - %s", username, nowPlayingTrack.Artist.Text, nowPlayingTrack.Name)
337
+
l.mu.Lock()
338
+
lastSeen, existed := l.lastSeenNowPlaying[username]
339
+
// if our current track matches with last seen
340
+
// just compare artist/album/name for now
341
+
if existed && lastSeen.Album == nowPlayingTrack.Album && lastSeen.Name == nowPlayingTrack.Name && lastSeen.Artist == nowPlayingTrack.Artist {
342
+
log.Printf("current track matches last seen track for %s", username)
343
+
} else {
344
+
log.Printf("current track does not match last seen track for %s", username)
345
+
// aha! we record this!
346
+
l.lastSeenNowPlaying[username] = nowPlayingTrack
347
+
}
348
+
l.mu.Unlock()
349
+
}
350
351
+
// find last non-now-playing track
352
+
var lastNonNowPlaying *Track
353
for i := len(tracks) - 1; i >= 0; i-- {
354
+
if tracks[i].Attr == nil || tracks[i].Attr.NowPlaying != "true" {
355
+
lastNonNowPlaying = &tracks[i]
356
+
break
357
+
}
358
+
}
359
+
360
+
if lastNonNowPlaying == nil {
361
+
log.Printf("no non-now-playing tracks found for user %s.", username)
362
+
return nil
363
+
}
364
+
365
+
uts, err := strconv.ParseInt(lastNonNowPlaying.Date.UTS, 10, 64)
366
+
if err != nil {
367
+
log.Printf("error parsing timestamp '%s' for track %s - %s: %v",
368
+
lastNonNowPlaying.Date.UTS, lastNonNowPlaying.Artist.Text, lastNonNowPlaying.Name, err)
369
+
}
370
+
latestTrackTime := time.Unix(uts, 0)
371
+
372
+
if found && lastKnownTimestamp.Equal(latestTrackTime) {
373
+
log.Printf("no new tracks to process for user %s.", username)
374
+
return nil
375
+
}
376
377
+
for _, track := range tracks {
378
+
if track.Attr != nil && track.Attr.NowPlaying == "true" {
379
+
continue // already handled separately
0
380
}
381
0
382
if track.Date == nil || track.Date.UTS == "" {
383
+
log.Printf("skipping track without timestamp for %s: %s - %s", username, track.Artist.Text, track.Name)
384
continue
385
}
386
0
387
uts, err := strconv.ParseInt(track.Date.UTS, 10, 64)
388
if err != nil {
389
+
log.Printf("error parsing timestamp '%s' for track %s - %s: %v", track.Date.UTS, track.Artist.Text, track.Name, err)
390
continue
391
}
392
trackTime := time.Unix(uts, 0)
393
394
+
if lastKnownTimestamp != nil && trackTime.Before(*lastKnownTimestamp) {
395
if processedCount == 0 {
396
+
log.Printf("reached already known scrobbles for user %s (track time: %s, last known: %s).",
397
+
username, trackTime.Format(time.RFC3339), lastKnownTimestamp.Format(time.RFC3339))
0
0
398
}
399
break
400
}
401
402
+
baseTrack := models.Track{
0
0
0
0
0
0
0
403
Name: track.Name,
404
URL: track.URL,
405
ServiceBaseUrl: "last.fm",
406
Album: track.Album.Text,
407
+
Timestamp: trackTime,
408
+
Artist: []models.Artist{
409
+
{
410
+
Name: track.Artist.Text,
411
+
MBID: track.Artist.MBID,
412
+
},
413
+
},
414
}
415
416
+
hydratedTrack, err := musicbrainz.HydrateTrack(l.musicBrainzService, baseTrack)
0
417
if err != nil {
418
+
log.Printf("error hydrating track for user %s: %s - %s: %v", username, track.Artist.Text, track.Name, err)
0
0
0
419
continue
420
}
421
422
+
l.db.SaveTrack(user.ID, hydratedTrack)
0
423
processedCount++
424
425
if trackTime.After(latestProcessedTime) {
···
432
}
433
434
if processedCount > 0 {
435
+
log.Printf("processed %d new track(s) for user %s. latest timestamp: %s",
436
processedCount, username, latestProcessedTime.Format(time.RFC3339))
437
}
438