[WIP] music platform user data scraper
teal-fm atproto

Merge pull request #3 from fatfingers23/bugfix/SpotifySync

Bugfix: Small spotify bugfix

authored by kyle loveless and committed by GitHub 71c82284 83b72c08

Changed files
+44 -30
db
service
lastfm
spotify
+1
.gitignore
··· 3 **/piper.db 4 jwk*.json 5 **.bak
··· 3 **/piper.db 4 jwk*.json 5 **.bak 6 + .idea
+2 -2
db/db.go
··· 159 user := &models.User{} 160 161 err := db.QueryRow(` 162 - SELECT id, username, email, spotify_id, access_token, refresh_token, token_expiry, lastfm_username, created_at, updated_at 163 FROM users WHERE id = ?`, ID).Scan( 164 - &user.ID, &user.Username, &user.Email, &user.SpotifyID, 165 &user.AccessToken, &user.RefreshToken, &user.TokenExpiry, 166 &user.LastFMUsername, 167 &user.CreatedAt, &user.UpdatedAt)
··· 159 user := &models.User{} 160 161 err := db.QueryRow(` 162 + SELECT id, username, email, atproto_did, spotify_id, access_token, refresh_token, token_expiry, lastfm_username, created_at, updated_at 163 FROM users WHERE id = ?`, ID).Scan( 164 + &user.ID, &user.Username, &user.Email, &user.ATProtoDID, &user.SpotifyID, 165 &user.AccessToken, &user.RefreshToken, &user.TokenExpiry, 166 &user.LastFMUsername, 167 &user.CreatedAt, &user.UpdatedAt)
+4 -15
service/lastfm/lastfm.go
··· 316 return nil 317 } 318 319 - uts, err := strconv.ParseInt(lastNonNowPlaying.Date.UTS, 10, 64) 320 - if err != nil { 321 - log.Printf("error parsing timestamp '%s' for track %s - %s: %v", 322 - lastNonNowPlaying.Date.UTS, lastNonNowPlaying.Artist.Text, lastNonNowPlaying.Name, err) 323 - } 324 - latestTrackTime := time.Unix(uts, 0) 325 326 // print both 327 fmt.Printf("latestTrackTime: %s\n", latestTrackTime) 328 fmt.Printf("lastKnownTimestamp: %s\n", lastKnownTimestamp) 329 330 - if lastKnownTimestamp != nil && lastKnownTimestamp.Equal(latestTrackTime) { 331 log.Printf("no new tracks to process for user %s.", username) 332 return nil 333 } 334 335 for _, track := range tracks { 336 - if track.Date == nil || track.Date.UTS == "" { 337 log.Printf("skipping track without timestamp for %s: %s - %s", username, track.Artist.Text, track.Name) 338 continue 339 } 340 341 - uts, err := strconv.ParseInt(track.Date.UTS, 10, 64) 342 - if err != nil { 343 - log.Printf("error parsing timestamp '%s' for track %s - %s: %v", track.Date.UTS, track.Artist.Text, track.Name, err) 344 - continue 345 - } 346 - trackTime := time.Unix(uts, 0) 347 - 348 // before or at last known 349 if lastKnownTimestamp != nil && (trackTime.Before(*lastKnownTimestamp) || trackTime.Equal(*lastKnownTimestamp)) { 350 if processedCount == 0 {
··· 316 return nil 317 } 318 319 + latestTrackTime := lastNonNowPlaying.Date 320 321 // print both 322 fmt.Printf("latestTrackTime: %s\n", latestTrackTime) 323 fmt.Printf("lastKnownTimestamp: %s\n", lastKnownTimestamp) 324 325 + if lastKnownTimestamp != nil && lastKnownTimestamp.Equal(latestTrackTime.Time) { 326 log.Printf("no new tracks to process for user %s.", username) 327 return nil 328 } 329 330 for _, track := range tracks { 331 + if track.Date == nil { 332 log.Printf("skipping track without timestamp for %s: %s - %s", username, track.Artist.Text, track.Name) 333 continue 334 } 335 336 + trackTime := track.Date.Time 337 // before or at last known 338 if lastKnownTimestamp != nil && (trackTime.Before(*lastKnownTimestamp) || trackTime.Equal(*lastKnownTimestamp)) { 339 if processedCount == 0 {
+36 -2
service/lastfm/model.go
··· 1 package lastfm 2 3 // Structs to represent the Last.fm API response for user.getrecenttracks 4 type RecentTracksResponse struct { 5 RecentTracks RecentTracks `json:"recenttracks"` ··· 19 Name string `json:"name"` 20 URL string `json:"url"` 21 Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields 22 - Attr *struct { // Custom handling for @attr.nowplaying 23 NowPlaying string `json:"nowplaying"` // Field name corrected to match struct tag 24 } `json:"@attr,omitempty"` // This captures the @attr object within the track 25 } ··· 39 Text string `json:"#text"` // Album name 40 } 41 42 - type TrackDate struct { 43 UTS string `json:"uts"` // Unix timestamp string 44 Text string `json:"#text"` // Human-readable date string 45 } 46 47 type TrackXMLAttr struct {
··· 1 package lastfm 2 3 + import ( 4 + "encoding/json" 5 + "strconv" 6 + "time" 7 + ) 8 + 9 // Structs to represent the Last.fm API response for user.getrecenttracks 10 type RecentTracksResponse struct { 11 RecentTracks RecentTracks `json:"recenttracks"` ··· 25 Name string `json:"name"` 26 URL string `json:"url"` 27 Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields 28 + Attr *struct { // Custom handling for @attr.nowplaying 29 NowPlaying string `json:"nowplaying"` // Field name corrected to match struct tag 30 } `json:"@attr,omitempty"` // This captures the @attr object within the track 31 } ··· 45 Text string `json:"#text"` // Album name 46 } 47 48 + // ApiTrackDate This is the real structure returned from lastFM. 49 + // Represents a date associated with a track, including both a Unix timestamp and a human-readable string. 50 + // UTS is a Unix timestamp stored as a string. 51 + // Text contains the human-readable date string. 52 + type ApiTrackDate struct { 53 UTS string `json:"uts"` // Unix timestamp string 54 Text string `json:"#text"` // Human-readable date string 55 + } 56 + 57 + // TrackDate This is the struct we use to represent a date associated with a track. 58 + // It is a wrapper around time.Time that implements json.Unmarshaler. 59 + type TrackDate struct { 60 + time.Time 61 + } 62 + 63 + // UnmarshalJSON Implements json.Unmarshaler. 64 + // Parses the UTS field from the API response and converts it to a time.Time. 65 + // The time.Time is stored in the Time field. 66 + // The Text field is ignored since it can be parsed from the Time field if needed. 67 + func (t *TrackDate) UnmarshalJSON(b []byte) (err error) { 68 + var apiTrackDate ApiTrackDate 69 + if err := json.Unmarshal(b, &apiTrackDate); err != nil { 70 + return err 71 + } 72 + uts, err := strconv.ParseInt(apiTrackDate.UTS, 10, 64) 73 + if err != nil { 74 + return err 75 + } 76 + date := time.Unix(uts, 0).UTC() 77 + t.Time = date 78 + return 79 } 80 81 type TrackXMLAttr struct {
+1 -11
service/spotify/spotify.go
··· 507 return nil, fmt.Errorf("failed to unmarshal spotify response: %w", err) 508 } 509 510 - body, ioErr := io.ReadAll(resp.Body) 511 - if ioErr != nil { 512 - return nil, ioErr 513 - } 514 - 515 - err = json.Unmarshal(body, &response) 516 - if err != nil { 517 - return nil, err 518 - } 519 - 520 var artists []models.Artist 521 for _, artist := range response.Item.Artists { 522 artists = append(artists, models.Artist{ ··· 536 ServiceBaseUrl: "open.spotify.com", 537 ISRC: response.Item.ExternalIDs.ISRC, 538 HasStamped: false, 539 - Timestamp: time.Now(), 540 } 541 542 return track, nil
··· 507 return nil, fmt.Errorf("failed to unmarshal spotify response: %w", err) 508 } 509 510 var artists []models.Artist 511 for _, artist := range response.Item.Artists { 512 artists = append(artists, models.Artist{ ··· 526 ServiceBaseUrl: "open.spotify.com", 527 ISRC: response.Item.ExternalIDs.ISRC, 528 HasStamped: false, 529 + Timestamp: time.Now().UTC(), 530 } 531 532 return track, nil