package main import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "teal-cider/auth" "teal-cider/types" "time" "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/client" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/lex/util" "github.com/teal-fm/piper/api/teal" ) const AGENT string = "teal-cider/0.0.2" var ENV_DATA envData = envData{} func getCurrentSong() (types.NowPlaying, error) { r := types.NowPlaying{} req, err := http.NewRequest("GET", "http://localhost:10767/api/v1/playback/now-playing", nil) if err != nil { return r, err } ENV_DATA.AddCiderHeader(req) res, err := http.DefaultClient.Do(req) if err != nil { return r, fmt.Errorf("Cannot connect to the Cider API, make sure Cider is launched and that the API is enabled") } if res.StatusCode != 200 { b, err := io.ReadAll(res.Body) if err != nil { return r, err } return r, fmt.Errorf("An error occured : %s", b) } err = json.NewDecoder(res.Body).Decode(&r) if err != nil { return r, err } return r, nil } func getMbRecord(query string) (types.MBRecord, error) { record := types.MBRecord{} req, err := http.NewRequest("GET", "https://musicbrainz.org/ws/2/recording", nil) if err != nil { return record, err } req.Header.Add("User-Agent", AGENT) req.Header.Add("content-type", "false") queryParam := req.URL.Query() queryParam.Add("fmt", "json") queryParam.Add("query", query) req.URL.RawQuery = queryParam.Encode() res, err := http.DefaultClient.Do(req) if err != nil { return record, err } err = json.NewDecoder(res.Body).Decode(&record) return record, err } func getInfos(song types.NowPlaying) (types.MBRecord, error) { if song.Info.Isrc != "" { isrc := song.Info.Isrc[len(song.Info.Isrc)-12:] query := fmt.Sprintf("isrc:\"%s\"", isrc) r, err := getMbRecord(query) if err == nil && r.Count > 0 { return r, nil } } query := fmt.Sprintf("recording:\"%s\" AND artist:\"%s\"", song.Info.Name, song.Info.ArtistName) r, err := getMbRecord(query) return r, err } func recordToTeal(records types.MBRecord, s types.NowPlaying) teal.AlphaFeedPlay { duration := int64(s.Info.DurationInMillis / 1000) dupAgent := string(AGENT) now := fmt.Sprintf("%d", time.Now().Unix()) if records.Count == 0 { artist := teal.AlphaFeedDefs_Artist{ ArtistName: s.Info.ArtistName, } artists := []*teal.AlphaFeedDefs_Artist{&artist} return teal.AlphaFeedPlay{ Artists: artists, TrackName: s.Info.Name, ReleaseName: &s.Info.AlbumName, Duration: &duration, PlayedTime: &now, SubmissionClientAgent: &dupAgent, } } else { r := records.Recordings[0] artists := make([]teal.AlphaFeedDefs_Artist, 0) for _, a := range r.ArtistCredit { artists = append(artists, teal.AlphaFeedDefs_Artist{ ArtistName: a.Artist.Name, ArtistMbId: &a.Artist.ID, }) } artistsRef := make([]*teal.AlphaFeedDefs_Artist, 0) for _, a := range artists { artistsRef = append(artistsRef, &a) } return teal.AlphaFeedPlay{ Artists: artistsRef, TrackName: r.Title, TrackMbId: &r.ID, ReleaseName: &r.Releases[0].Title, ReleaseMbId: &r.Releases[0].ID, Duration: &duration, PlayedTime: &now, SubmissionClientAgent: &dupAgent, } } } type envData struct { ciderToken string appPassword string handle string } func getEnvData() envData { return envData{ ciderToken: os.Getenv("CIDER_TOKEN"), appPassword: os.Getenv("APP_PASSWORD"), handle: os.Getenv("HANDLE"), } } func (e envData) AddCiderHeader(r *http.Request) { if e.ciderToken != "" { r.Header.Add("apptoken", e.ciderToken) } } func (e envData) HasAppPassword() bool { return e.appPassword != "" } func (e envData) Login() *client.APIClient { handle, err := syntax.ParseHandle(e.handle) if err != nil { panic(err) } ident := syntax.AtIdentifier{ Inner: handle, } c, err := client.LoginWithPassword(context.Background(), identity.DefaultDirectory(), ident, e.appPassword, "", nil) if err != nil { panic(err) } return c } func createPlay(client *client.APIClient, playRecord teal.AlphaFeedPlay) error { entry := atproto.RepoCreateRecord_Input{ Collection: "fm.teal.alpha.feed.play", Repo: string(*client.AccountDID), Record: &util.LexiconTypeDecoder{Val: &playRecord}, } a := atproto.RepoCreateRecord_Output{} err := client.Post(context.Background(), "com.atproto.repo.createRecord", entry, &a) if err != nil { return err } return nil } func getClient() *client.APIClient { if ENV_DATA.HasAppPassword() { return ENV_DATA.Login() } else { authChan := make(chan *oauth.ClientSession) go auth.StartServer(authChan) session := <-authChan if session == nil { panic("Could not connect to ATProto") } return session.APIClient() } } func main() { ENV_DATA = getEnvData() client := getClient() // LOGIC var lastPlaying string = "" for { s, err := getCurrentSong() if err != nil { fmt.Fprintln(os.Stderr, err.Error()) } else if s.Info.Name != "" && lastPlaying != s.Info.PlayParams.ID { r, err := getInfos(s) if err == nil { t := recordToTeal(r, s) createPlay(client, t) lastPlaying = s.Info.PlayParams.ID } else { fmt.Fprintln(os.Stderr, err.Error()) } } time.Sleep(10 * time.Second) } }