A fork of https://github.com/teal-fm/piper

lfm: handle now playing track separately

Natalie B 45e05bef 9d2d675c

+89 -44
+15
models/atproto.go
··· 1 1 package models 2 2 3 3 import ( 4 + "time" 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 + 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 42 Name string `json:"name"` 43 43 URL string `json:"url"` 44 44 Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields 45 - NowPlaying *struct { // Custom handling for @attr.nowplaying 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 - // 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 85 + lastSeenNowPlaying map[string]Track 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 + lastSeenNowPlaying: make(map[string]Track), 102 + mu: sync.Mutex{}, 102 103 } 103 104 } 104 105 ··· 302 303 } 303 304 } 304 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 - // 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 - lastKnownTimestamp, err := l.db.GetLastScrobbleTimestamp(user.ID) // Hypothetical DB call 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 - log.Printf("No previous scrobble timestamp found for user %s. Processing latest track.", username) 323 + log.Printf("no previous scrobble timestamp found for user %s. processing latest track.", username) 325 324 } else { 326 - log.Printf("Last known scrobble for %s was at %s", username, lastKnownTimestamp.Format(time.RFC3339)) 325 + log.Printf("last known scrobble for %s was at %s", username, lastKnownTimestamp.Format(time.RFC3339)) 327 326 } 328 327 329 - processedCount := 0 330 - var latestProcessedTime time.Time 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 + } 331 350 351 + // find last non-now-playing track 352 + var lastNonNowPlaying *Track 332 353 for i := len(tracks) - 1; i >= 0; i-- { 333 - track := tracks[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 + } 334 376 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 377 + for _, track := range tracks { 378 + if track.Attr != nil && track.Attr.NowPlaying == "true" { 379 + continue // already handled separately 339 380 } 340 381 341 - // skip tracks w/out valid date (should be none, but just in case) 342 382 if track.Date == nil || track.Date.UTS == "" { 343 - log.Printf("Skipping track without timestamp for %s: %s - %s", username, track.Artist.Text, track.Name) 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 - // parse uts (unix timestamp string) 348 387 uts, err := strconv.ParseInt(track.Date.UTS, 10, 64) 349 388 if err != nil { 350 - log.Printf("Error parsing timestamp '%s' for track %s - %s: %v", track.Date.UTS, track.Artist.Text, track.Name, err) 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 - if lastKnownTimestamp != nil && !trackTime.After(*lastKnownTimestamp) { 394 + if lastKnownTimestamp != nil && trackTime.Before(*lastKnownTimestamp) { 356 395 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)) 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)) 361 398 } 362 399 break 363 400 } 364 401 365 - unhydratedArtist := []models.Artist{ 366 - { 367 - Name: track.Artist.Text, 368 - MBID: track.Artist.MBID, 369 - }, 370 - } 371 - 372 - mTrack := models.Track{ 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 - Timestamp: time.Unix(uts, 0), 378 - Artist: unhydratedArtist, 407 + Timestamp: trackTime, 408 + Artist: []models.Artist{ 409 + { 410 + Name: track.Artist.Text, 411 + MBID: track.Artist.MBID, 412 + }, 413 + }, 379 414 } 380 415 381 - // Fix based on diagnostic: Assume HydrateTrack returns (*models.Track, error) 382 - hydratedTrackPtr, err := musicbrainz.HydrateTrack(l.musicBrainzService, mTrack) 416 + hydratedTrack, err := musicbrainz.HydrateTrack(l.musicBrainzService, baseTrack) 383 417 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 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 - l.db.SaveTrack(user.ID, hydratedTrackPtr) 392 - 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 - log.Printf("Successfully processed %d new track(s) for user %s. Latest timestamp in batch: %s", 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