A way to send current playing track in cider to teal collection
1package main
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "os"
10 "teal-cider/auth"
11 "teal-cider/types"
12 "time"
13
14 "github.com/bluesky-social/indigo/api/atproto"
15 "github.com/bluesky-social/indigo/atproto/auth/oauth"
16 "github.com/bluesky-social/indigo/atproto/client"
17 "github.com/bluesky-social/indigo/atproto/identity"
18 "github.com/bluesky-social/indigo/atproto/syntax"
19 "github.com/bluesky-social/indigo/lex/util"
20 "github.com/teal-fm/piper/api/teal"
21)
22
23const AGENT string = "teal-cider/0.0.2"
24
25var ENV_DATA envData = envData{}
26
27func getCurrentSong() (types.NowPlaying, error) {
28 r := types.NowPlaying{}
29 req, err := http.NewRequest("GET", "http://localhost:10767/api/v1/playback/now-playing", nil)
30 if err != nil {
31 return r, err
32 }
33
34 ENV_DATA.AddCiderHeader(req)
35
36 res, err := http.DefaultClient.Do(req)
37 if err != nil {
38 return r, fmt.Errorf("Cannot connect to the Cider API, make sure Cider is launched and that the API is enabled")
39 }
40
41 if res.StatusCode != 200 {
42 b, err := io.ReadAll(res.Body)
43 if err != nil {
44 return r, err
45 }
46 return r, fmt.Errorf("An error occured : %s", b)
47 }
48
49 err = json.NewDecoder(res.Body).Decode(&r)
50 if err != nil {
51 return r, err
52 }
53
54 return r, nil
55}
56
57func getMbRecord(query string) (types.MBRecord, error) {
58 record := types.MBRecord{}
59 req, err := http.NewRequest("GET", "https://musicbrainz.org/ws/2/recording", nil)
60 if err != nil {
61 return record, err
62 }
63
64 req.Header.Add("User-Agent", AGENT)
65 req.Header.Add("content-type", "false")
66
67 queryParam := req.URL.Query()
68 queryParam.Add("fmt", "json")
69 queryParam.Add("query", query)
70 req.URL.RawQuery = queryParam.Encode()
71
72 res, err := http.DefaultClient.Do(req)
73 if err != nil {
74 return record, err
75 }
76
77 err = json.NewDecoder(res.Body).Decode(&record)
78 return record, err
79}
80
81func getInfos(song types.NowPlaying) (types.MBRecord, error) {
82 if song.Info.Isrc != "" {
83 isrc := song.Info.Isrc[len(song.Info.Isrc)-12:]
84 query := fmt.Sprintf("isrc:\"%s\"", isrc)
85 r, err := getMbRecord(query)
86 if err == nil && r.Count > 0 {
87 return r, nil
88 }
89 }
90
91 query := fmt.Sprintf("recording:\"%s\" AND artist:\"%s\"", song.Info.Name, song.Info.ArtistName)
92 r, err := getMbRecord(query)
93 return r, err
94}
95
96func recordToTeal(records types.MBRecord, s types.NowPlaying) teal.AlphaFeedPlay {
97 duration := int64(s.Info.DurationInMillis / 1000)
98 dupAgent := string(AGENT)
99 now := fmt.Sprintf("%d", time.Now().Unix())
100
101 if records.Count == 0 {
102 artist := teal.AlphaFeedDefs_Artist{
103 ArtistName: s.Info.ArtistName,
104 }
105 artists := []*teal.AlphaFeedDefs_Artist{&artist}
106 return teal.AlphaFeedPlay{
107 Artists: artists,
108 TrackName: s.Info.Name,
109 ReleaseName: &s.Info.AlbumName,
110 Duration: &duration,
111 PlayedTime: &now,
112 SubmissionClientAgent: &dupAgent,
113 }
114 } else {
115 r := records.Recordings[0]
116
117 artists := make([]teal.AlphaFeedDefs_Artist, 0)
118 for _, a := range r.ArtistCredit {
119 artists = append(artists, teal.AlphaFeedDefs_Artist{
120 ArtistName: a.Artist.Name,
121 ArtistMbId: &a.Artist.ID,
122 })
123 }
124
125 artistsRef := make([]*teal.AlphaFeedDefs_Artist, 0)
126 for _, a := range artists {
127 artistsRef = append(artistsRef, &a)
128 }
129
130 return teal.AlphaFeedPlay{
131 Artists: artistsRef,
132 TrackName: r.Title,
133 TrackMbId: &r.ID,
134 ReleaseName: &r.Releases[0].Title,
135 ReleaseMbId: &r.Releases[0].ID,
136 Duration: &duration,
137 PlayedTime: &now,
138 SubmissionClientAgent: &dupAgent,
139 }
140 }
141}
142
143type envData struct {
144 ciderToken string
145 appPassword string
146 handle string
147}
148
149func getEnvData() envData {
150 return envData{
151 ciderToken: os.Getenv("CIDER_TOKEN"),
152 appPassword: os.Getenv("APP_PASSWORD"),
153 handle: os.Getenv("HANDLE"),
154 }
155}
156
157func (e envData) AddCiderHeader(r *http.Request) {
158 if e.ciderToken != "" {
159 r.Header.Add("apptoken", e.ciderToken)
160 }
161}
162
163func (e envData) HasAppPassword() bool {
164 return e.appPassword != ""
165}
166
167func (e envData) Login() *client.APIClient {
168 handle, err := syntax.ParseHandle(e.handle)
169 if err != nil {
170 panic(err)
171 }
172
173 ident := syntax.AtIdentifier{
174 Inner: handle,
175 }
176 c, err := client.LoginWithPassword(context.Background(), identity.DefaultDirectory(), ident, e.appPassword, "", nil)
177 if err != nil {
178 panic(err)
179 }
180 return c
181}
182
183func createPlay(client *client.APIClient, playRecord teal.AlphaFeedPlay) error {
184 entry := atproto.RepoCreateRecord_Input{
185 Collection: "fm.teal.alpha.feed.play",
186 Repo: string(*client.AccountDID),
187 Record: &util.LexiconTypeDecoder{Val: &playRecord},
188 }
189
190 a := atproto.RepoCreateRecord_Output{}
191 err := client.Post(context.Background(), "com.atproto.repo.createRecord", entry, &a)
192 if err != nil {
193 return err
194 }
195
196 return nil
197}
198
199func getClient() *client.APIClient {
200 if ENV_DATA.HasAppPassword() {
201 return ENV_DATA.Login()
202 } else {
203 authChan := make(chan *oauth.ClientSession)
204 go auth.StartServer(authChan)
205 session := <-authChan
206 if session == nil {
207 panic("Could not connect to ATProto")
208 }
209 return session.APIClient()
210 }
211}
212
213func main() {
214 ENV_DATA = getEnvData()
215 client := getClient()
216
217 // LOGIC
218 var lastPlaying string = ""
219 for {
220 s, err := getCurrentSong()
221 if err != nil {
222 fmt.Fprintln(os.Stderr, err.Error())
223 } else if s.Info.Name != "" && lastPlaying != s.Info.PlayParams.ID {
224 r, err := getInfos(s)
225
226 if err == nil {
227 t := recordToTeal(r, s)
228 createPlay(client, t)
229 lastPlaying = s.Info.PlayParams.ID
230 } else {
231 fmt.Fprintln(os.Stderr, err.Error())
232 }
233 }
234 time.Sleep(10 * time.Second)
235 }
236}