+1
-1
db/atproto.go
+1
-1
db/atproto.go
+72
-25
db/db.go
+72
-25
db/db.go
···
3
3
import (
4
4
"database/sql"
5
5
"encoding/json"
6
+
"fmt"
6
7
"os"
7
8
"path/filepath"
8
9
"time"
···
374
375
return users, nil
375
376
}
376
377
377
-
func (db *DB) AddLastFMUsername(userID int64, lastfmUsername string) error {
378
-
_, err := db.Exec(`
379
-
UPDATE users
380
-
SET lastfm_username = ?
381
-
WHERE user_id = ?`, lastfmUsername, userID)
378
+
// debug to view current user's information
379
+
// put everything in an 'any' type
380
+
func (db *DB) DebugViewUserInformation(userID int64) (map[string]any, error) {
381
+
// Use Query instead of QueryRow to get access to column names and ensure only one row is processed
382
+
rows, err := db.Query(`
383
+
SELECT *
384
+
FROM users
385
+
WHERE id = ? LIMIT 1`, userID)
386
+
if err != nil {
387
+
return nil, fmt.Errorf("query failed: %w", err)
388
+
}
389
+
defer rows.Close()
382
390
383
-
return err
384
-
}
391
+
// Get column names
392
+
cols, err := rows.Columns()
393
+
if err != nil {
394
+
return nil, fmt.Errorf("failed to get columns: %w", err)
395
+
}
385
396
386
-
func (db *DB) GetAllUsersWithLastFM() ([]*models.User, error) {
387
-
rows, err := db.Query(`
388
-
SELECT id, username, email, spotify_id, access_token, refresh_token, token_expiry, created_at, updated_at, lastfm_username
389
-
FROM users
390
-
ORDER BY id`)
397
+
// Check if there's a row to process
398
+
if !rows.Next() {
399
+
if err := rows.Err(); err != nil {
400
+
// Error during rows.Next() or preparing the result set
401
+
return nil, fmt.Errorf("error checking for row: %w", err)
402
+
}
403
+
// No rows found, which is a valid outcome but might be considered an error in some contexts.
404
+
// Returning sql.ErrNoRows is conventional.
405
+
return nil, sql.ErrNoRows
406
+
}
391
407
408
+
// Prepare scan arguments: pointers to interface{} slices
409
+
values := make([]any, len(cols))
410
+
scanArgs := make([]any, len(cols))
411
+
for i := range values {
412
+
scanArgs[i] = &values[i]
413
+
}
414
+
415
+
// Scan the row values
416
+
err = rows.Scan(scanArgs...)
392
417
if err != nil {
393
-
return nil, err
418
+
return nil, fmt.Errorf("failed to scan row: %w", err)
394
419
}
395
-
defer rows.Close()
396
420
397
-
var users []*models.User
421
+
// Check for errors that might have occurred during iteration (after Scan)
422
+
if err := rows.Err(); err != nil {
423
+
return nil, fmt.Errorf("error after scanning row: %w", err)
424
+
}
398
425
399
-
for rows.Next() {
400
-
user := &models.User{}
401
-
err := rows.Scan(
402
-
&user.ID, &user.Username, &user.Email, &user.SpotifyID,
403
-
&user.AccessToken, &user.RefreshToken, &user.TokenExpiry,
404
-
&user.CreatedAt, &user.UpdatedAt, &user.LastFMUsername)
405
-
if err != nil {
406
-
return nil, err
426
+
// Create the result map
427
+
resultMap := make(map[string]any, len(cols))
428
+
for i, colName := range cols {
429
+
val := values[i]
430
+
// SQLite often returns []byte for TEXT columns, convert to string for usability.
431
+
// Also handle potential nil values appropriately.
432
+
if b, ok := val.([]byte); ok {
433
+
resultMap[colName] = string(b)
434
+
} else {
435
+
resultMap[colName] = val // Keep nil as nil, numbers as numbers, etc.
407
436
}
408
-
users = append(users, user)
409
437
}
410
438
411
-
return users, nil
439
+
return resultMap, nil
440
+
}
441
+
442
+
func (db *DB) GetLastScrobbleTimestamp(userID int64) (*time.Time, error) {
443
+
var lastTimestamp time.Time
444
+
err := db.QueryRow(`
445
+
SELECT timestamp
446
+
FROM tracks
447
+
WHERE user_id = ?
448
+
ORDER BY timestamp DESC
449
+
LIMIT 1`, userID).Scan(&lastTimestamp)
450
+
451
+
if err != nil {
452
+
if err == sql.ErrNoRows {
453
+
return nil, nil
454
+
}
455
+
return nil, fmt.Errorf("failed to query last scrobble timestamp for user %d: %w", userID, err)
456
+
}
457
+
458
+
return &lastTimestamp, nil
412
459
}
+29
-5
main.go
+29
-5
main.go
···
146
146
147
147
func handleLinkLastfmForm(database *db.DB) http.HandlerFunc {
148
148
return func(w http.ResponseWriter, r *http.Request) {
149
-
userID, _ := session.GetUserID(r.Context()) // Auth middleware ensures this exists
149
+
userID, _ := session.GetUserID(r.Context())
150
+
if r.Method == http.MethodPost {
151
+
if err := r.ParseForm(); err != nil {
152
+
http.Error(w, "Failed to parse form", http.StatusBadRequest)
153
+
return
154
+
}
155
+
156
+
lastfmUsername := r.FormValue("lastfm_username")
157
+
if lastfmUsername == "" {
158
+
http.Error(w, "Last.fm username cannot be empty", http.StatusBadRequest)
159
+
return
160
+
}
161
+
162
+
err := database.AddLastFMUsername(userID, lastfmUsername)
163
+
if err != nil {
164
+
log.Printf("Error saving Last.fm username for user %d: %v", userID, err)
165
+
http.Error(w, "Failed to save Last.fm username", http.StatusInternalServerError)
166
+
return
167
+
}
168
+
169
+
log.Printf("Successfully linked Last.fm username '%s' for user ID %d", lastfmUsername, userID)
170
+
171
+
http.Redirect(w, r, "/", http.StatusSeeOther)
172
+
}
150
173
151
174
currentUser, err := database.GetUserByID(userID)
152
175
currentUsername := ""
···
337
360
338
361
mbService := musicbrainz.NewMusicBrainzService(database)
339
362
spotifyService := spotify.NewSpotifyService(database, atprotoService, mbService)
340
-
lastfmService := lastfm.NewLastFMService(database, viper.GetString("lastfm.api_key"))
363
+
lastfmService := lastfm.NewLastFMService(database, viper.GetString("lastfm.api_key"), mbService)
341
364
342
-
sessionManager := session.NewSessionManager()
343
-
oauthManager := oauth.NewOAuthServiceManager()
365
+
sessionManager := session.NewSessionManager(database)
366
+
oauthManager := oauth.NewOAuthServiceManager(sessionManager)
344
367
345
368
spotifyOAuth := oauth.NewOAuth2Service(
346
369
viper.GetString("spotify.client_id"),
···
369
392
http.HandleFunc("/api-keys", session.WithAuth(apiKeyService.HandleAPIKeyManagement, sessionManager))
370
393
http.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(database), sessionManager)) // GET form
371
394
http.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(database), sessionManager)) // POST submit - Changed route slightly
372
-
http.HandleFunc("/logout", sessionManager.HandleLogout) // Logout doesn't strictly need auth middleware, but handles session deletion
395
+
http.HandleFunc("/logout", sessionManager.HandleLogout)
396
+
http.HandleFunc("/debug/", session.WithAuth(sessionManager.HandleDebug, sessionManager))
373
397
374
398
http.HandleFunc("/api/v1/current-track", session.WithAPIAuth(apiCurrentTrack(spotifyService), sessionManager)) // Spotify Current
375
399
http.HandleFunc("/api/v1/history", session.WithAPIAuth(apiTrackHistory(spotifyService), sessionManager)) // Spotify History
+1
-1
models/user.go
+1
-1
models/user.go
+2
-2
oauth/oauth_manager.go
+2
-2
oauth/oauth_manager.go
···
17
17
mu sync.RWMutex
18
18
}
19
19
20
-
func NewOAuthServiceManager() *OAuthServiceManager {
20
+
func NewOAuthServiceManager(sessionManager *session.SessionManager) *OAuthServiceManager {
21
21
return &OAuthServiceManager{
22
22
services: make(map[string]AuthService),
23
-
sessionManager: session.NewSessionManager(),
23
+
sessionManager: sessionManager,
24
24
}
25
25
}
26
26
+194
-31
service/lastfm/lastfm.go
+194
-31
service/lastfm/lastfm.go
···
9
9
"net/http"
10
10
"net/url"
11
11
"strconv"
12
+
"sync"
12
13
"time"
13
14
14
15
"github.com/teal-fm/piper/db"
16
+
"github.com/teal-fm/piper/models"
17
+
"github.com/teal-fm/piper/service/musicbrainz"
15
18
"golang.org/x/time/rate"
16
19
)
17
20
18
21
const (
19
22
lastfmAPIBaseURL = "https://ws.audioscrobbler.com/2.0/"
20
-
defaultLimit = 50 // Default number of tracks to fetch per user
23
+
defaultLimit = 1 // Default number of tracks to fetch per user
21
24
)
22
25
23
26
// Structs to represent the Last.fm API response for user.getrecenttracks
···
40
43
URL string `json:"url"`
41
44
Date *TrackDate `json:"date,omitempty"` // Use pointer for optional fields
42
45
NowPlaying *struct { // Custom handling for @attr.nowplaying
43
-
NowPlaying string `json:"nowplaying"`
44
-
} `json:"@attr,omitempty"`
46
+
NowPlaying string `json:"nowplaying"` // Field name corrected to match struct tag
47
+
} `json:"@attr,omitempty"` // This captures the @attr object within the track
45
48
}
46
49
47
50
type Artist struct {
···
73
76
}
74
77
75
78
type LastFMService struct {
76
-
db *db.DB
77
-
httpClient *http.Client
78
-
limiter *rate.Limiter
79
-
apiKey string
80
-
Usernames []string
79
+
db *db.DB
80
+
httpClient *http.Client
81
+
limiter *rate.Limiter
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
81
88
}
82
89
83
-
func NewLastFMService(db *db.DB, apiKey string) *LastFMService {
90
+
func NewLastFMService(db *db.DB, apiKey string, musicBrainzService *musicbrainz.MusicBrainzService) *LastFMService {
84
91
return &LastFMService{
85
92
db: db,
86
93
httpClient: &http.Client{
···
90
97
limiter: rate.NewLimiter(rate.Every(200*time.Millisecond), 1),
91
98
apiKey: apiKey,
92
99
Usernames: make([]string, 0),
100
+
// lastSeenTrackDate: make(map[string]time.Time), // Removed
101
+
musicBrainzService: musicBrainzService,
93
102
}
94
103
}
95
104
···
135
144
params.Set("user", username)
136
145
params.Set("api_key", l.apiKey)
137
146
params.Set("format", "json")
138
-
params.Set("limit", strconv.Itoa(limit))
147
+
params.Set("limit", strconv.Itoa(limit)) // Fetch a few more to handle duplicates/now playing
139
148
140
149
apiURL := lastfmAPIBaseURL + "?" + params.Encode()
141
150
···
157
166
158
167
if resp.StatusCode != http.StatusOK {
159
168
bodyBytes, _ := io.ReadAll(resp.Body)
169
+
// Handle specific Last.fm error codes if necessary
170
+
// e.g., {"error": 6, "message": "User not found"}
160
171
return nil, fmt.Errorf("last.fm API error for %s: status %d, body: %s", username, resp.StatusCode, string(bodyBytes))
161
172
}
162
173
163
174
var recentTracksResp RecentTracksResponse
164
-
if err := json.NewDecoder(resp.Body).Decode(&recentTracksResp); err != nil {
175
+
bodyBytes, err := io.ReadAll(resp.Body) // Read body first for potential decoding errors
176
+
if err != nil {
177
+
return nil, fmt.Errorf("failed to read response body for %s: %w", username, err)
178
+
}
179
+
if err := json.Unmarshal(bodyBytes, &recentTracksResp); err != nil {
180
+
// Log the body content that failed to decode
181
+
log.Printf("Failed to decode response body for %s: %s", username, string(bodyBytes))
165
182
return nil, fmt.Errorf("failed to decode response for %s: %w", username, err)
166
183
}
167
184
···
181
198
func (l *LastFMService) StartListeningTracker(interval time.Duration) {
182
199
if err := l.loadUsernames(); err != nil {
183
200
log.Printf("Failed to perform initial username load: %v", err)
201
+
// Decide if we should proceed without initial load or return error
184
202
}
185
203
186
204
if len(l.Usernames) == 0 {
187
-
log.Println("No Last.fm users configured! Will start listening tracker anyways.")
205
+
log.Println("No Last.fm users configured. Tracker will run but fetch cycles will be skipped until users are added.")
206
+
} else {
207
+
log.Printf("Found %d Last.fm users.", len(l.Usernames))
188
208
}
189
209
190
210
ticker := time.NewTicker(interval)
191
211
go func() {
192
-
l.fetchAllUserTracks(context.Background())
212
+
// Initial fetch immediately
213
+
if len(l.Usernames) > 0 {
214
+
l.fetchAllUserTracks(context.Background())
215
+
} else {
216
+
log.Println("Skipping initial fetch cycle as no users are configured.")
217
+
}
193
218
194
219
for {
195
220
select {
···
197
222
// refresh usernames periodically from db
198
223
if err := l.loadUsernames(); err != nil {
199
224
log.Printf("Error reloading usernames in ticker: %v", err)
225
+
// Continue ticker loop even if reload fails? Or log and potentially stop?
226
+
continue // Continue for now
200
227
}
201
228
if len(l.Usernames) > 0 {
202
229
l.fetchAllUserTracks(context.Background())
203
230
} else {
204
231
log.Println("No Last.fm users configured. Skipping fetch cycle.")
205
232
}
206
-
// Add a way to stop the goroutine if needed, e.g., via a context or channel
233
+
// TODO: Implement graceful shutdown using context cancellation
207
234
// case <-ctx.Done():
208
235
// log.Println("Stopping Last.fm listening tracker.")
209
236
// ticker.Stop()
···
218
245
// fetchAllUserTracks iterates through users and fetches their tracks.
219
246
func (l *LastFMService) fetchAllUserTracks(ctx context.Context) {
220
247
log.Printf("Starting fetch cycle for %d users...", len(l.Usernames))
248
+
var wg sync.WaitGroup // Use WaitGroup to fetch concurrently (optional)
249
+
fetchErrors := make(chan error, len(l.Usernames)) // Channel for errors
250
+
221
251
for _, username := range l.Usernames {
222
252
if ctx.Err() != nil {
223
-
log.Printf("Context cancelled during fetch cycle for user %s.", username)
224
-
return
253
+
log.Printf("Context cancelled before starting fetch for user %s.", username)
254
+
break // Exit loop if context is cancelled
255
+
}
256
+
257
+
wg.Add(1)
258
+
go func(uname string) { // Launch fetch and process in a goroutine per user
259
+
defer wg.Done()
260
+
if ctx.Err() != nil {
261
+
log.Printf("Context cancelled during fetch cycle for user %s.", uname)
262
+
return // Exit goroutine if context is cancelled
263
+
}
264
+
265
+
// Fetch slightly more than 1 track to better handle edge cases
266
+
// where the latest is 'now playing' or duplicates exist.
267
+
const fetchLimit = 5
268
+
recentTracks, err := l.getRecentTracks(ctx, uname, fetchLimit)
269
+
if err != nil {
270
+
log.Printf("Error fetching tracks for %s: %v", uname, err)
271
+
fetchErrors <- fmt.Errorf("fetch failed for %s: %w", uname, err) // Report error
272
+
return
273
+
}
274
+
275
+
if recentTracks == nil || len(recentTracks.RecentTracks.Tracks) == 0 {
276
+
log.Printf("No tracks returned for user %s", uname)
277
+
return
278
+
}
279
+
280
+
// Process the fetched tracks
281
+
if err := l.processTracks(uname, recentTracks.RecentTracks.Tracks); err != nil {
282
+
log.Printf("Error processing tracks for %s: %v", uname, err)
283
+
fetchErrors <- fmt.Errorf("process failed for %s: %w", uname, err) // Report error
284
+
}
285
+
}(username)
286
+
}
287
+
288
+
wg.Wait() // Wait for all goroutines to complete
289
+
close(fetchErrors) // Close the error channel
290
+
291
+
// Log any errors that occurred during the fetch cycle
292
+
errorCount := 0
293
+
for err := range fetchErrors {
294
+
log.Printf("Fetch cycle error: %v", err)
295
+
errorCount++
296
+
}
297
+
298
+
if errorCount > 0 {
299
+
log.Printf("Finished fetch cycle with %d errors.", errorCount)
300
+
} else {
301
+
log.Println("Finished fetch cycle successfully.")
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
331
+
332
+
for i := len(tracks) - 1; i >= 0; i-- {
333
+
track := tracks[i]
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
225
339
}
226
-
recentTracks, err := l.getRecentTracks(ctx, username, defaultLimit)
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)
227
349
if err != nil {
228
-
log.Printf("Error fetching tracks for %s: %v", username, err)
350
+
log.Printf("Error parsing timestamp '%s' for track %s - %s: %v", track.Date.UTS, track.Artist.Text, track.Name, err)
229
351
continue
230
352
}
353
+
trackTime := time.Unix(uts, 0)
231
354
232
-
// TODO: Process the fetched tracks (e.g., store in DB, update stats)
233
-
_ = recentTracks // Avoid unused variable warning for now
234
-
// Example processing:
235
-
// l.processTracks(username, recentTracks.RecentTracks.Tracks)
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
+
}
236
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,
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) {
396
+
latestProcessedTime = trackTime
397
+
}
398
+
399
+
if found {
400
+
break
401
+
}
237
402
}
238
-
log.Println("Finished fetch cycle.")
239
-
}
240
403
241
-
// Placeholder for processing logic
242
-
// func (l *LastFMService) processTracks(username string, tracks []Track) {
243
-
// log.Printf("Processing %d tracks for user %s", len(tracks), username)
244
-
// // Implement logic to store tracks, update user scrobble counts, etc.
245
-
// // Consider handling 'now playing' tracks differently (track.NowPlaying != nil)
246
-
// // Be mindful of duplicates if the interval is short. Store the last fetched timestamp?
247
-
// }
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
+
409
+
return nil
410
+
}
+62
service/musicbrainz/musicbrainz.go
+62
service/musicbrainz/musicbrainz.go
···
3
3
import (
4
4
"context"
5
5
"encoding/json"
6
+
"errors"
6
7
"fmt"
7
8
"log"
8
9
"net/http"
···
13
14
"time"
14
15
15
16
"github.com/teal-fm/piper/db"
17
+
"github.com/teal-fm/piper/models"
16
18
"golang.org/x/time/rate"
17
19
)
18
20
···
253
255
r := releases[0]
254
256
return &r
255
257
}
258
+
259
+
func HydrateTrack(mb *MusicBrainzService, track models.Track) (*models.Track, error) {
260
+
ctx := context.Background()
261
+
// array of strings
262
+
artistArray := make([]string, len(track.Artist)) // Assuming Name is string type
263
+
for i, a := range track.Artist {
264
+
artistArray[i] = a.Name
265
+
}
266
+
267
+
params := SearchParams{
268
+
Track: track.Name,
269
+
Artist: strings.Join(artistArray, ", "),
270
+
Release: track.Album,
271
+
}
272
+
res, err := mb.SearchMusicBrainz(ctx, params)
273
+
if err != nil {
274
+
return nil, err
275
+
}
276
+
277
+
if len(res) == 0 {
278
+
return nil, errors.New("no results found")
279
+
}
280
+
281
+
firstResult := res[0]
282
+
firstResultAlbum := GetBestRelease(firstResult.Releases, firstResult.Title)
283
+
284
+
bestISRC := firstResult.ISRCs[0]
285
+
286
+
if len(firstResult.ISRCs) == 0 {
287
+
bestISRC = track.ISRC
288
+
}
289
+
290
+
artists := make([]models.Artist, len(firstResult.ArtistCredit))
291
+
292
+
for i, a := range firstResult.ArtistCredit {
293
+
artists[i] = models.Artist{
294
+
Name: a.Name,
295
+
ID: a.Artist.ID,
296
+
MBID: a.Artist.ID,
297
+
}
298
+
}
299
+
300
+
resTrack := models.Track{
301
+
HasStamped: track.HasStamped,
302
+
PlayID: track.PlayID,
303
+
Name: track.Name,
304
+
URL: track.URL,
305
+
ServiceBaseUrl: track.ServiceBaseUrl,
306
+
RecordingMBID: firstResult.ID,
307
+
Album: firstResultAlbum.Title,
308
+
ReleaseMBID: firstResultAlbum.ID,
309
+
ISRC: bestISRC,
310
+
Timestamp: track.Timestamp,
311
+
ProgressMs: track.ProgressMs,
312
+
DurationMs: int64(firstResult.Length),
313
+
Artist: artists,
314
+
}
315
+
316
+
return &resTrack, nil
317
+
}
+6
-71
service/spotify/spotify.go
+6
-71
service/spotify/spotify.go
···
1
1
package spotify
2
2
3
3
import (
4
-
"context"
5
-
"encoding/base64" // Added for Basic Auth
4
+
"encoding/base64"
6
5
"encoding/json"
7
6
"errors"
8
7
"fmt"
9
8
"io"
10
9
"log"
11
10
"net/http"
12
-
"net/url" // Added for request body
13
-
14
-
// "strconv" // Removed unused import
15
-
"strings" // Added for request body
11
+
"net/url"
12
+
"strings"
16
13
"sync"
17
14
"time"
18
15
19
-
"github.com/spf13/viper" // Added for config access
16
+
"github.com/spf13/viper"
20
17
"github.com/teal-fm/piper/db"
21
18
"github.com/teal-fm/piper/models"
22
19
"github.com/teal-fm/piper/oauth/atproto"
···
89
86
// for now log and continue
90
87
log.Printf("Error updating user token for user ID %d: %v", user.ID, err)
91
88
} else {
92
-
log.Printf("Updated token for existing user: %s (ID: %d)", user.Username, user.ID)
89
+
log.Printf("Updated token for existing user: %s (ID: %d)", *user.Username, user.ID)
93
90
}
94
91
}
95
92
user.AccessToken = &token
···
99
96
s.userTokens[user.ID] = token
100
97
s.mu.Unlock()
101
98
102
-
log.Printf("User authenticated via Spotify: %s (ID: %d)", user.Username, user.ID)
99
+
log.Printf("User authenticated via Spotify: %s (ID: %d)", *user.Username, user.ID)
103
100
return user.ID, nil
104
101
}
105
102
···
552
549
553
550
track.PlayID = id
554
551
555
-
// Update in memory
556
552
s.mu.Lock()
557
553
s.userTracks[userID] = track
558
554
s.mu.Unlock()
···
562
558
}
563
559
}
564
560
}
565
-
566
-
// Take a track, with any IDs and hydrate it with MusicBrainz data
567
-
func (s *SpotifyService) hydrateTrack(track *models.Track) (*models.Track, error) {
568
-
ctx := context.Background()
569
-
// array of strings
570
-
artistArray := make([]string, len(track.Artist)) // Assuming Name is string type
571
-
for i, a := range track.Artist {
572
-
artistArray[i] = a.Name
573
-
}
574
-
575
-
params := musicbrainz.SearchParams{
576
-
Track: track.Name,
577
-
Artist: strings.Join(artistArray, ", "),
578
-
Release: track.Album,
579
-
}
580
-
res, err := s.mb.SearchMusicBrainz(ctx, params)
581
-
if err != nil {
582
-
return nil, err
583
-
}
584
-
585
-
if len(res) == 0 {
586
-
return nil, errors.New("no results found")
587
-
}
588
-
589
-
firstResult := res[0]
590
-
firstResultAlbum := musicbrainz.GetBestRelease(firstResult.Releases, firstResult.Title)
591
-
592
-
bestISRC := firstResult.ISRCs[0]
593
-
594
-
if len(firstResult.ISRCs) == 0 {
595
-
bestISRC = track.ISRC
596
-
}
597
-
598
-
artists := make([]models.Artist, len(firstResult.ArtistCredit))
599
-
600
-
for i, a := range firstResult.ArtistCredit {
601
-
artists[i] = models.Artist{
602
-
Name: a.Name,
603
-
ID: a.Artist.ID,
604
-
MBID: a.Artist.ID,
605
-
}
606
-
}
607
-
608
-
resTrack := models.Track{
609
-
HasStamped: track.HasStamped,
610
-
PlayID: track.PlayID,
611
-
Name: track.Name,
612
-
URL: track.URL,
613
-
ServiceBaseUrl: track.ServiceBaseUrl,
614
-
RecordingMBID: firstResult.ID,
615
-
Album: firstResultAlbum.Title,
616
-
ReleaseMBID: firstResultAlbum.ID,
617
-
ISRC: bestISRC,
618
-
Timestamp: track.Timestamp,
619
-
ProgressMs: track.ProgressMs,
620
-
DurationMs: int64(firstResult.Length),
621
-
Artist: artists,
622
-
}
623
-
624
-
return &resTrack, nil
625
-
}
+27
-10
session/session.go
+27
-10
session/session.go
···
4
4
"context"
5
5
"crypto/rand"
6
6
"encoding/base64"
7
+
"encoding/json"
8
+
"fmt"
7
9
"log"
8
10
"net/http"
9
11
"sync"
···
31
33
mu sync.RWMutex
32
34
}
33
35
34
-
func NewSessionManager() *SessionManager {
35
-
database, err := db.New("./data/piper.db")
36
-
if err != nil {
37
-
log.Printf("Error connecting to database for sessions, falling back to in memory only: %v", err)
38
-
// back up to in memory only
39
-
return &SessionManager{
40
-
sessions: make(map[string]*Session),
41
-
}
42
-
}
36
+
func NewSessionManager(database *db.DB) *SessionManager {
43
37
44
-
_, err = database.Exec(`
38
+
_, err := database.Exec(`
45
39
CREATE TABLE IF NOT EXISTS sessions (
46
40
id TEXT PRIMARY KEY,
47
41
user_id INTEGER NOT NULL,
···
305
299
306
300
handler(w, r)
307
301
}
302
+
}
303
+
304
+
func (sm *SessionManager) HandleDebug(w http.ResponseWriter, r *http.Request) {
305
+
ctx := r.Context()
306
+
userID, ok := GetUserID(ctx)
307
+
if !ok {
308
+
w.Header().Set("Content-Type", "application/json")
309
+
w.WriteHeader(http.StatusUnauthorized)
310
+
w.Write([]byte(`{"error": "User ID not found in context"}`))
311
+
return
312
+
}
313
+
314
+
res, err := sm.db.DebugViewUserInformation(userID)
315
+
if err != nil {
316
+
w.Header().Set("Content-Type", "application/json")
317
+
w.WriteHeader(http.StatusInternalServerError)
318
+
w.Write([]byte(fmt.Sprintf(`{"error": "Failed to retrieve user information: %v"}`, err)))
319
+
return
320
+
}
321
+
322
+
w.Header().Set("Content-Type", "application/json")
323
+
w.WriteHeader(http.StatusOK)
324
+
json.NewEncoder(w).Encode(res)
308
325
}
309
326
310
327
type contextKey int