소스 검색

feat: route to fetch artists

Fela Maslen 5 년 전
부모
커밋
cc7a20a580

+ 34 - 0
music-player/pkg/repository/songs.go

@@ -2,6 +2,7 @@ package repository
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/felamaslen/go-music-player/pkg/read"
 	"github.com/jmoiron/sqlx"
@@ -10,6 +11,8 @@ import (
 
 const BATCH_SIZE = 100
 
+const UNKNOWN_ARTIST = "Unknown Artist"
+
 func SelectSong(db *sqlx.DB, id int) (song *read.Song, err error) {
   var songs []*read.Song
 
@@ -36,6 +39,37 @@ func SelectSong(db *sqlx.DB, id int) (song *read.Song, err error) {
   return
 }
 
+func SelectPagedArtists(db *sqlx.DB, limit int, offset int) (artists *[]string, err error) {
+  artists = &[]string{}
+  err = db.Select(artists, fmt.Sprintf(`
+  select distinct
+    case when artist = '' then '%s' else artist end as artist
+  from songs
+  order by artist
+  limit $1
+  offset $2
+  `, UNKNOWN_ARTIST), limit, offset)
+  return
+}
+
+type CountRow struct {
+  Count int `db:"count"`
+}
+
+func SelectArtistCount(db *sqlx.DB) (count int, err error) {
+  var countRow CountRow
+
+  err = db.QueryRowx(`
+  select count(*) as count from (
+    select distinct artist from songs
+  ) distinct_artists
+  `).StructScan(&countRow)
+
+  count = countRow.Count
+
+  return
+}
+
 func BatchUpsertSongs(db *sqlx.DB, batch *[BATCH_SIZE]*read.Song, batchSize int) error {
   var trackNumbers pq.Int64Array
   var titles pq.StringArray

+ 51 - 0
music-player/pkg/server/fetch.go

@@ -0,0 +1,51 @@
+package server
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/felamaslen/go-music-player/pkg/logger"
+	"github.com/felamaslen/go-music-player/pkg/services"
+	"github.com/go-redis/redis/v7"
+)
+
+type ArtistsResponse struct {
+  Artists []string 	`json:"artists"`
+  More bool 		`json:"more"`
+}
+
+func routeFetchArtists(l *logger.Logger, rdb *redis.Client, w http.ResponseWriter, r *http.Request) error {
+  limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
+  if err != nil {
+    http.Error(w, "Limit must be an integer", http.StatusBadRequest)
+    return nil
+  }
+  if limit < 1 || limit > 1000 {
+    http.Error(w, "Limit must be between 1 and 1000", http.StatusBadRequest)
+    return nil
+  }
+
+  page, err := strconv.Atoi(r.URL.Query().Get("page"))
+  if err != nil {
+    http.Error(w, "Page must be an integer", http.StatusBadRequest)
+    return nil
+  }
+  if page < 0 {
+    http.Error(w, "Page must be non-negative", http.StatusBadRequest)
+    return nil
+  }
+
+  artists, more := services.GetArtists(limit, page)
+
+  response, err := json.Marshal(ArtistsResponse{
+    Artists: *artists,
+    More: more,
+  })
+  if err != nil {
+    return err
+  }
+
+  w.Write(response)
+  return nil
+}

+ 1 - 2
music-player/pkg/server/handler.go

@@ -21,8 +21,7 @@ func routeHandler(
     if err != nil {
       l.Error("Unhandled error during request: %v\n", err)
 
-      w.WriteHeader(500)
-      w.Write([]byte("Unhandled error"))
+      http.Error(w, "Unhandled error", http.StatusInternalServerError)
     }
   }
 }

+ 2 - 0
music-player/pkg/server/server.go

@@ -24,6 +24,8 @@ func StartServer() {
 
   router.Path("/stream").Methods("GET").HandlerFunc(routeHandler(l, rdb, streamSong))
 
+  router.Path("/artists").Methods("GET").HandlerFunc(routeHandler(l, rdb, routeFetchArtists))
+
   port := conf.Port
 
   l.Info("Starting server on port %d\n", port)

+ 24 - 0
music-player/pkg/services/fetch.go

@@ -0,0 +1,24 @@
+package services
+
+import (
+	"github.com/felamaslen/go-music-player/pkg/database"
+	"github.com/felamaslen/go-music-player/pkg/repository"
+)
+
+func GetArtists(limit int, page int) (artists *[]string, more bool) {
+  db := database.GetConnection()
+
+  artists, err := repository.SelectPagedArtists(db, limit, limit * page)
+  if err != nil {
+    panic(err)
+  }
+
+  total, err := repository.SelectArtistCount(db)
+  if err != nil {
+    panic(err)
+  }
+
+  more = limit * (1 + page) < total
+
+  return
+}

+ 136 - 0
music-player/pkg/services/fetch_test.go

@@ -0,0 +1,136 @@
+package services_test
+
+import (
+	"fmt"
+
+	"github.com/lib/pq"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/felamaslen/go-music-player/pkg/database"
+	"github.com/felamaslen/go-music-player/pkg/services"
+	setup "github.com/felamaslen/go-music-player/pkg/testing"
+)
+
+var _ = Describe("Fetching data", func() {
+  db := database.GetConnection()
+
+  BeforeEach(func() {
+    setup.PrepareDatabaseForTesting()
+  })
+
+  Describe("getArtists", func() {
+    var insertArtists = func(artists []string) {
+      var trackNumbers = make([]int, len(artists))
+      var titles = make([]string, len(artists))
+      var albums = make([]string, len(artists))
+      var durations = make([]int, len(artists))
+      var basePaths = make([]string, len(artists))
+      var relativePaths = make([]string, len(artists))
+      var modifiedDates = make([]int, len(artists))
+
+      for i := 0; i < len(artists); i++ {
+	trackNumbers[i] = i + 1
+	titles[i] = fmt.Sprintf("Title %d", i + 1)
+	albums[i] = fmt.Sprintf("Album %d", i + 1)
+	durations[i] = 403 + i
+	basePaths[i] = "/music"
+	relativePaths[i] = fmt.Sprintf("file%d.ogg", i)
+	modifiedDates[i] = 177712347 + i
+      }
+
+      db.MustExec(
+	`
+	insert into songs (
+	  track_number
+	  ,title
+	  ,artist
+	  ,album
+	  ,duration
+	  ,modified_date
+	  ,base_path
+	  ,relative_path
+	)
+	select * from unnest(
+	  $1::integer[]
+	  ,$2::varchar[]
+	  ,$3::varchar[]
+	  ,$4::varchar[]
+	  ,$5::integer[]
+	  ,$6::bigint[]
+	  ,$7::varchar[]
+	  ,$8::varchar[]
+	)
+	`,
+	pq.Array(trackNumbers),
+	pq.Array(titles),
+	pq.Array(artists),
+	pq.Array(albums),
+	pq.Array(durations),
+	pq.Array(modifiedDates),
+	pq.Array(basePaths),
+	pq.Array(relativePaths),
+      )
+    }
+
+    Context("when there are no songs", func() {
+      It("should return an empty slice and set more to false", func() {
+	artists, more := services.GetArtists(100, 0)
+
+	Expect(*artists).To(HaveLen(0))
+	Expect(more).To(BeFalse())
+      })
+    })
+
+    Context("when there are no songs with artists", func() {
+      BeforeEach(func() {
+	insertArtists([]string{"", ""})
+      })
+
+      It("should return Unknown Artist", func() {
+	artists, more := services.GetArtists(100, 0)
+
+	Expect(*artists).To(HaveLen(1))
+	Expect((*artists)[0]).To(Equal("Unknown Artist"))
+	Expect(more).To(BeFalse())
+      })
+    })
+
+    Context("when there are fewer artists than the limit given", func() {
+      BeforeEach(func() {
+	insertArtists([]string{"Artist A", "Artist B", "Artist C", "Artist D"})
+      })
+
+      It("should return an ordered set matching the limit", func() {
+	artists, _ := services.GetArtists(3, 0)
+
+	Expect(*artists).To(HaveLen(3))
+
+	Expect((*artists)[0]).To(Equal("Artist A"))
+	Expect((*artists)[1]).To(Equal("Artist B"))
+	Expect((*artists)[2]).To(Equal("Artist C"))
+      })
+
+      It("should set more to true", func() {
+	_, more := services.GetArtists(3, 0)
+
+	Expect(more).To(BeTrue())
+      })
+
+      Context("when paging", func() {
+	It("should return the next set of results", func() {
+	  artists, _ := services.GetArtists(3, 1)
+
+	  Expect(*artists).To(HaveLen(1))
+	  Expect((*artists)[0]).To(Equal("Artist D"))
+	})
+
+	It("should set more to false at the end", func() {
+	  _, more := services.GetArtists(3, 1)
+
+	  Expect(more).To(BeFalse())
+	})
+      })
+    })
+  })
+})