diff --git a/client/mpd/djmpd/djmpd.go b/client/mpd/djmpd/djmpd.go new file mode 100644 index 0000000000000000000000000000000000000000..9308a6f32d4fde26d55d4f7108aed8eabf710449 --- /dev/null +++ b/client/mpd/djmpd/djmpd.go @@ -0,0 +1,23 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "git.autistici.org/ale/gompd" + "git.autistici.org/ale/djrandom/client/mpd" +) + +var ( + port = flag.Int("port", 6600, "Port to listen on (MPD)") +) + +func main() { + flag.Parse() + + db := djmpd.NewDJRandomDatabase() + m := mpd.NewMpd(mpd.NewPortAudioPlayer(), db) + m.HandleURL("djrandom", db) + log.Fatal(m.ListenAndServe(fmt.Sprintf(":%d", *port))) +} diff --git a/client/mpd/djrandom.go b/client/mpd/djrandom.go new file mode 100644 index 0000000000000000000000000000000000000000..b5ef82db2c7ded3d1077312e4f492404f2e099a7 --- /dev/null +++ b/client/mpd/djrandom.go @@ -0,0 +1,205 @@ +package djmpd + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "log" + "net/url" + "strings" + "sync" + + "git.autistici.org/ale/djrandom/api" + "git.autistici.org/ale/djrandom/client" + "git.autistici.org/ale/djrandom/util" + "git.autistici.org/ale/djrandom/util/config" + + "git.autistici.org/ale/gompd" +) + +var ( + configFile = flag.String("djrandom-config", util.ExpandTilde("~/.djrandom.conf"), "Config file location") + + serverUrl = config.String("djrandom-server", "https://djrandom.incal.net", "Server URL") + authKeyId = config.String("auth_key", "", "API authentication key") + authKeySecret = config.String("auth_secret", "", "API authentication secret") + + djrandomOnce sync.Once +) + +type DJRandomSong struct { + *api.Song + + audiofile *api.AudioFile + client *util.HttpClient +} + +func newDJRandomSong(song *api.Song, client *util.HttpClient) *DJRandomSong { + af := song.GetBestAudioFile() + return &DJRandomSong{ + Song: song, + audiofile: af, + client: client, + } +} + +func (s *DJRandomSong) Channels() int { + if s.audiofile != nil { + return s.audiofile.Channels + } + return 2 +} + +func (s *DJRandomSong) SampleRate() float64 { + if s.audiofile != nil { + return float64(s.audiofile.SampleRate) + } + return 44100 +} + +func (s *DJRandomSong) URL() string { + return fmt.Sprintf("djrandom://%s", s.Id.String()) +} + +func (s *DJRandomSong) Info() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "file: %s\n", s.URL()) + fmt.Fprintf(&buf, "Last-Modified: 2010-12-16T18:02:14Z\n") + if s.Meta.Artist != "" { + fmt.Fprintf(&buf, "Artist: %s\n", s.Meta.Artist) + } + if s.Meta.Title != "" { + fmt.Fprintf(&buf, "Title: %s\n", s.Meta.Title) + } + if s.Meta.Album != "" { + fmt.Fprintf(&buf, "Album: %s\n", s.Meta.Album) + } + if s.Meta.Genre != "" { + fmt.Fprintf(&buf, "Genre: %s\n", s.Meta.Genre) + } + if s.Meta.Year > 0 { + fmt.Fprintf(&buf, "Date: %d\n", s.Meta.Year) + } + if s.Meta.TrackNum > 0 { + fmt.Fprintf(&buf, "Track: %d\n", s.Meta.TrackNum) + } + if s.audiofile != nil { + fmt.Fprintf(&buf, "Time: %d\n", int(s.audiofile.Duration)) + } + return buf.String() +} + +func (s *DJRandomSong) Open() (io.ReadCloser, error) { + if s.audiofile == nil { + return nil, errors.New("no audio file") + } + + resp, err := s.client.GetRaw("/dl/" + s.audiofile.MD5) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + resp.Body.Close() + return nil, fmt.Errorf("HTTP error %d", resp.StatusCode) + } + return resp.Body, nil +} + +type DJRandomDatabase struct { + client *util.HttpClient +} + +func NewDJRandomDatabase() *DJRandomDatabase { + djrandomOnce.Do(func() { + if err := config.Parse(*configFile); err != nil { + log.Printf("Warning: could not read DJRandom config file: %v", err) + } + }) + + authKey := &api.AuthKey{ + KeyId: *authKeyId, + Secret: *authKeySecret, + } + client := util.NewHttpClient(*serverUrl, authKey, client.LoadCA()) + return &DJRandomDatabase{ + client: client, + } +} + +func (d *DJRandomDatabase) doQuery(query string) ([]mpd.Song, error) { + log.Printf("literal query: %s", query) + + values := url.Values{} + values.Add("q", query) + req, err := d.client.NewRequest("POST", "/api/search", strings.NewReader(values.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + var resp api.SearchResponse + if err := d.client.DoJSON(nil, req, &resp); err != nil { + return nil, err + } + + // Convert []api.Song to []Song. + var out []mpd.Song + for _, s := range resp.Results { + out = append(out, newDJRandomSong(s, d.client)) + } + return out, nil +} + +func buildQuery(args []string, exact bool) string { + var qprefix string + if exact { + qprefix = "=" + } + i := 0 + var qparts []string + for i < len(args) { + i++ + if i >= len(args) { + break + } + what := strings.ToLower(args[i-1]) + q := args[i] + if what == "any" { + qparts = append(qparts, fmt.Sprintf("\"%s%s\"", qprefix, q)) + } else { + qparts = append(qparts, fmt.Sprintf("%s:\"%s%s\"", what, qprefix, q)) + } + i++ + } + return strings.Join(qparts, " ") +} + +func (d *DJRandomDatabase) Search(args []string) ([]mpd.Song, error) { + return d.doQuery(buildQuery(args, false)) +} + +func (d *DJRandomDatabase) Find(args []string) ([]mpd.Song, error) { + return d.doQuery(buildQuery(args, true)) +} + +func (d *DJRandomDatabase) GetSong(songURL *url.URL) (mpd.Song, error) { + songID := songURL.Host + if songID == "" { + return nil, errors.New("empty song ID") + } + + req, err := d.client.NewRequest("GET", "/api/song/"+songID, nil) + if err != nil { + return nil, err + } + + var resp api.Song + if err := d.client.DoJSON(nil, req, &resp); err != nil { + return nil, err + } + + // Convert api.Song to Song. + return newDJRandomSong(&resp, d.client), nil +}