A way to send current playing track in cider to teal collection
at main 5.6 kB view raw
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}