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