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
submission for spotify
Natalie B
10 months ago
13ef2f68
9b59f5ea
+127
-5
2 changed files
expand all
collapse all
unified
split
service
lastfm
lastfm.go
spotify
spotify.go
+5
-1
service/lastfm/lastfm.go
···
16
16
"github.com/bluesky-social/indigo/api/atproto"
17
17
lexutil "github.com/bluesky-social/indigo/lex/util"
18
18
"github.com/bluesky-social/indigo/xrpc"
19
19
+
"github.com/spf13/viper"
19
20
"github.com/teal-fm/piper/api/teal"
20
21
"github.com/teal-fm/piper/db"
21
22
"github.com/teal-fm/piper/models"
···
434
435
}
435
436
436
437
playedTimeStr := track.Timestamp.Format(time.RFC3339)
437
437
-
submissionAgent := "piper/v0.0.1" // TODO: get this from the environment on compilation
438
438
+
submissionAgent := viper.GetString("app.submission_agent")
439
439
+
if submissionAgent == "" {
440
440
+
submissionAgent = "piper/v0.0.1" // Default if not configured
441
441
+
}
438
442
439
443
// track -> tealfm track
440
444
tfmTrack := teal.AlphaFeedPlay{
+122
-4
service/spotify/spotify.go
···
13
13
"sync"
14
14
"time"
15
15
16
16
+
"context" // Added for context.Context
17
17
+
18
18
+
"github.com/bluesky-social/indigo/api/atproto" // Added for atproto.RepoCreateRecord_Input
19
19
+
lexutil "github.com/bluesky-social/indigo/lex/util" // Added for lexutil.LexiconTypeDecoder
20
20
+
"github.com/bluesky-social/indigo/xrpc" // Added for xrpc.Client
16
21
"github.com/spf13/viper"
22
22
+
"github.com/teal-fm/piper/api/teal" // Added for teal.AlphaFeedPlay
17
23
"github.com/teal-fm/piper/db"
18
24
"github.com/teal-fm/piper/models"
19
19
-
"github.com/teal-fm/piper/oauth/atproto"
25
25
+
atprotoauth "github.com/teal-fm/piper/oauth/atproto"
20
26
"github.com/teal-fm/piper/service/musicbrainz"
21
27
"github.com/teal-fm/piper/session"
22
28
)
23
29
24
30
type SpotifyService struct {
25
31
DB *db.DB
26
26
-
atprotoService *atproto.ATprotoAuthService // Added field
32
32
+
atprotoService *atprotoauth.ATprotoAuthService // Added field
27
33
mb *musicbrainz.MusicBrainzService // Added field
28
34
userTracks map[int64]*models.Track
29
35
userTokens map[int64]string
30
36
mu sync.RWMutex
31
37
}
32
38
33
33
-
func NewSpotifyService(database *db.DB, atprotoService *atproto.ATprotoAuthService, musicBrainzService *musicbrainz.MusicBrainzService) *SpotifyService {
39
39
+
func NewSpotifyService(database *db.DB, atprotoService *atprotoauth.ATprotoAuthService, musicBrainzService *musicbrainz.MusicBrainzService) *SpotifyService {
34
40
return &SpotifyService{
35
41
DB: database,
36
42
atprotoService: atprotoService,
···
40
46
}
41
47
}
42
48
49
49
+
func (s *SpotifyService) SubmitTrackToPDS(did string, track *models.Track, ctx context.Context) error {
50
50
+
client, err := s.atprotoService.GetATProtoClient()
51
51
+
if err != nil || client == nil {
52
52
+
log.Printf("Error getting ATProto client: %v", err)
53
53
+
return fmt.Errorf("failed to get ATProto client: %w", err)
54
54
+
}
55
55
+
56
56
+
xrpcClient := s.atprotoService.GetXrpcClient()
57
57
+
if xrpcClient == nil {
58
58
+
return errors.New("xrpc client is not available")
59
59
+
}
60
60
+
61
61
+
sess, err := s.DB.GetAtprotoSession(did, ctx, *client)
62
62
+
if err != nil {
63
63
+
return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err)
64
64
+
}
65
65
+
66
66
+
artistArr := make([]string, 0, len(track.Artist))
67
67
+
artistMbIdArr := make([]string, 0, len(track.Artist))
68
68
+
for _, a := range track.Artist {
69
69
+
artistArr = append(artistArr, a.Name)
70
70
+
artistMbIdArr = append(artistMbIdArr, a.MBID)
71
71
+
}
72
72
+
73
73
+
var durationPtr *int64
74
74
+
if track.DurationMs > 0 {
75
75
+
durationSeconds := track.DurationMs / 1000
76
76
+
durationPtr = &durationSeconds
77
77
+
}
78
78
+
79
79
+
playedTimeStr := track.Timestamp.Format(time.RFC3339)
80
80
+
submissionAgent := viper.GetString("app.submission_agent")
81
81
+
if submissionAgent == "" {
82
82
+
submissionAgent = "piper/v0.0.1" // Default if not configured
83
83
+
}
84
84
+
85
85
+
tfmTrack := teal.AlphaFeedPlay{
86
86
+
LexiconTypeID: "fm.teal.alpha.feed.play",
87
87
+
Duration: durationPtr,
88
88
+
TrackName: track.Name,
89
89
+
PlayedTime: &playedTimeStr,
90
90
+
ArtistNames: artistArr,
91
91
+
ArtistMbIds: artistMbIdArr,
92
92
+
ReleaseMbId: &track.ReleaseMBID,
93
93
+
ReleaseName: &track.Album,
94
94
+
RecordingMbId: &track.RecordingMBID,
95
95
+
// Optional: Spotify specific data if your lexicon supports it
96
96
+
// SpotifyTrackID: &track.ServiceID,
97
97
+
// SpotifyAlbumID: &track.ServiceAlbumID,
98
98
+
// SpotifyArtistIDs: track.ServiceArtistIDs, // Assuming this is a []string
99
99
+
SubmissionClientAgent: &submissionAgent,
100
100
+
}
101
101
+
102
102
+
input := atproto.RepoCreateRecord_Input{
103
103
+
Collection: "fm.teal.alpha.feed.play", // Ensure this collection is correct
104
104
+
Repo: sess.DID,
105
105
+
Record: &lexutil.LexiconTypeDecoder{Val: &tfmTrack},
106
106
+
}
107
107
+
108
108
+
authArgs := db.AtpSessionToAuthArgs(sess)
109
109
+
110
110
+
var out atproto.RepoCreateRecord_Output
111
111
+
if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
112
112
+
log.Printf("Error creating record for DID %s: %v. Input: %+v", did, err, input)
113
113
+
return fmt.Errorf("failed to create record on PDS for DID %s: %w", did, err)
114
114
+
}
115
115
+
116
116
+
log.Printf("Successfully submitted track '%s' to PDS for DID %s. Record URI: %s", track.Name, did, out.Uri)
117
117
+
return nil
118
118
+
}
119
119
+
43
120
func (s *SpotifyService) SetAccessToken(token string, userId int64, hasSession bool) (int64, error) {
44
121
userID, err := s.identifyAndStoreUser(token, userId, hasSession)
45
122
if err != nil {
···
347
424
var err error
348
425
349
426
// Retry logic: try once, if 401, refresh and try again
350
350
-
for attempt := 0; attempt < 2; attempt++ {
427
427
+
for attempt := range 2 {
351
428
// We need to be able to re-read the body if the request is retried,
352
429
// but since this is a GET request with no body, we don't need to worry about it.
353
430
resp, err = client.Do(req) // Use = instead of := inside loop
···
552
629
s.mu.Lock()
553
630
s.userTracks[userID] = track
554
631
s.mu.Unlock()
632
632
+
633
633
+
// Submit to ATProto PDS
634
634
+
// The 'track' variable is *models.Track and has been saved to DB, PlayID is populated.
635
635
+
dbUser, errUser := s.DB.GetUserByID(userID) // Fetch user by their internal ID
636
636
+
if errUser != nil {
637
637
+
log.Printf("User %d: Error fetching user details for PDS submission: %v", userID, errUser)
638
638
+
} else if dbUser == nil {
639
639
+
log.Printf("User %d: User not found in DB. Skipping PDS submission.", userID)
640
640
+
} else if dbUser.ATProtoDID == nil || *dbUser.ATProtoDID == "" {
641
641
+
log.Printf("User %d (%d): ATProto DID not set. Skipping PDS submission for track '%s'.", userID, dbUser.ATProtoDID, track.Name)
642
642
+
} else {
643
643
+
// User has a DID, proceed with hydration and submission
644
644
+
var trackToSubmitToPDS *models.Track = track // Default to the original track (already *models.Track)
645
645
+
if s.mb != nil { // Check if MusicBrainz service is available
646
646
+
// musicbrainz.HydrateTrack expects models.Track as second argument, so we pass *track
647
647
+
// and it returns *models.Track
648
648
+
hydratedTrack, errHydrate := musicbrainz.HydrateTrack(s.mb, *track)
649
649
+
if errHydrate != nil {
650
650
+
log.Printf("User %d (%d): Error hydrating track '%s' with MusicBrainz: %v. Proceeding with original track data for PDS.", userID, dbUser.ATProtoDID, track.Name, errHydrate)
651
651
+
} else {
652
652
+
log.Printf("User %d (%d): Successfully hydrated track '%s' with MusicBrainz.", userID, dbUser.ATProtoDID, track.Name)
653
653
+
trackToSubmitToPDS = hydratedTrack // hydratedTrack is *models.Track
654
654
+
}
655
655
+
} else {
656
656
+
log.Printf("User %d (%d): MusicBrainz service not configured. Proceeding with original track data for PDS.", userID, dbUser.ATProtoDID)
657
657
+
}
658
658
+
659
659
+
artistName := "Unknown Artist"
660
660
+
if len(trackToSubmitToPDS.Artist) > 0 {
661
661
+
artistName = trackToSubmitToPDS.Artist[0].Name
662
662
+
}
663
663
+
664
664
+
log.Printf("User %d (%d): Attempting to submit track '%s' by %s to PDS (DID: %s)", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, artistName, *dbUser.ATProtoDID)
665
665
+
// Use context.Background() for now, or pass down a context if available
666
666
+
if errPDS := s.SubmitTrackToPDS(*dbUser.ATProtoDID, trackToSubmitToPDS, context.Background()); errPDS != nil {
667
667
+
log.Printf("User %d (%d): Error submitting track '%s' to PDS: %v", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, errPDS)
668
668
+
} else {
669
669
+
log.Printf("User %d (%d): Successfully submitted track '%s' to PDS.", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name)
670
670
+
}
671
671
+
}
672
672
+
// End of PDS submission block
555
673
556
674
log.Printf("User %d is listening to: %s by %s", userID, track.Name, track.Artist)
557
675
}