Ver código fonte

feat: real time library watcher

    * feat: optional environment config to disable watching library
    * fix: run watcher in subroutine
    * fix: handle error when deleting song
    * feat: listen for file creation events and scan files when they happen
    * feat: watch for file deletes / renames
    * feat: repository function to delete a song by its path
    * refactor: files function organisation
    * refactor: moved catch-all services code into particular packages
Fela Maslen 4 anos atrás
pai
commit
ee92f060a2

+ 2 - 2
gmus-backend/cmd/gmus.scan/main.go

@@ -3,7 +3,7 @@ package main
 import (
 	"github.com/felamaslen/gmus-backend/pkg/config"
 	"github.com/felamaslen/gmus-backend/pkg/database"
-	"github.com/felamaslen/gmus-backend/pkg/services"
+	"github.com/felamaslen/gmus-backend/pkg/read"
 )
 
 func main() {
@@ -12,7 +12,7 @@ func main() {
 		panic("Must set LIBRARY_DIRECTORY")
 	}
 
-	services.ScanAndInsert(libraryDirectory)
+	read.ScanAndInsert(libraryDirectory)
 
 	database.EndPool()
 }

+ 1 - 0
gmus-backend/go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/anyhon/engine v0.1.0
 	github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc
 	github.com/elliotchance/redismock v1.5.3
+	github.com/fsnotify/fsnotify v1.4.9
 	github.com/go-playground/validator/v10 v10.4.1
 	github.com/go-redis/redis v6.15.9+incompatible
 	github.com/gofrs/uuid v3.2.0+incompatible // indirect

+ 6 - 0
gmus-backend/pkg/config/config.go

@@ -142,10 +142,16 @@ func getAllowedOrigins() []string {
 	return strings.Split(origins, ",")
 }
 
+func getLibraryWatch() bool {
+	watch, _ := os.LookupEnv("LIBRARY_WATCH")
+	return watch != "false"
+}
+
 type config struct {
 	DatabaseUrl      string
 	LogLevel         logger.LogLevel
 	LibraryDirectory string
+	LibraryWatch     bool
 	Host             string
 	Port             int
 	RedisUrl         string

+ 1 - 0
gmus-backend/pkg/config/main.go

@@ -11,6 +11,7 @@ func GetConfig() config {
 		DatabaseUrl:      getDatabaseUrl(),
 		LogLevel:         getLogLevel(),
 		LibraryDirectory: os.Getenv("LIBRARY_DIRECTORY"),
+		LibraryWatch:     getLibraryWatch(),
 		Host:             getListenHost(),
 		Port:             getPort(),
 		RedisUrl:         getRedisUrl(),

+ 8 - 0
gmus-backend/pkg/read/README.md

@@ -0,0 +1,8 @@
+# read
+
+This package contains logic for the following:
+
+- Reading file metadata
+- Recursively scanning directories
+
+Adding resulting information to the database is the responsibility of the [repository](../repository) package.

+ 1 - 1
gmus-backend/pkg/read/audio_test.go

@@ -12,7 +12,7 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-var _ = Describe("reading audio files", func() {
+var _ = Describe("Reading audio files", func() {
 
 	rootDir, _ := os.Getwd()
 	basePath := path.Join(rootDir, read.TestDirectory)

+ 4 - 0
gmus-backend/pkg/read/constants.go

@@ -0,0 +1,4 @@
+package read
+
+const LOG_EVERY = 100
+const BATCH_SIZE = 100

+ 17 - 32
gmus-backend/pkg/read/files.go

@@ -2,6 +2,7 @@ package read
 
 import (
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"time"
 
@@ -14,9 +15,6 @@ import (
 	"github.com/lib/pq"
 )
 
-const BATCH_SIZE = 100
-const LOG_EVERY = 100
-
 func ReadMultipleFiles(basePath string, files chan *types.File) chan *types.Song {
 	var db = database.GetConnection()
 	var l = logger.CreateLogger(config.GetConfig().LogLevel)
@@ -62,6 +60,18 @@ func isValidFile(file string) bool {
 	return filepath.Ext(file) == ".ogg"
 }
 
+func GetFileInfo(file os.FileInfo, relativePath string) *types.File {
+	if !isValidFile(file.Name()) {
+		return nil
+	}
+
+	return &types.File{
+		RelativePath: relativePath,
+		ModifiedDate: file.ModTime().Unix(),
+	}
+}
+
+// Utilities to aid directory reading
 func recursiveDirScan(
 	db *sqlx.DB,
 	l *logger.Logger,
@@ -102,10 +112,10 @@ func recursiveDirScan(
 				fileRelativePath,
 				false,
 			)
-		} else if isValidFile(file.Name()) {
-			*allFiles <- &types.File{
-				RelativePath: fileRelativePath,
-				ModifiedDate: file.ModTime().Unix(),
+		} else {
+			validFile := GetFileInfo(file, fileRelativePath)
+			if validFile != nil {
+				*allFiles <- validFile
 			}
 		}
 	}
@@ -180,28 +190,3 @@ func batchFilterFiles(
 		}
 	}
 }
-
-func ScanDirectory(directory string) chan *types.File {
-	db := database.GetConnection()
-	l := logger.CreateLogger(config.GetConfig().LogLevel)
-
-	filteredOutput := make(chan *types.File)
-	allFiles := make(chan *types.File)
-
-	go func() {
-		batchFilterFiles(db, l, &filteredOutput, &allFiles, directory)
-	}()
-
-	go func() {
-		recursiveDirScan(
-			db,
-			l,
-			&allFiles,
-			directory,
-			"",
-			true,
-		)
-	}()
-
-	return filteredOutput
-}

+ 2 - 84
gmus-backend/pkg/read/files_test.go

@@ -1,9 +1,6 @@
 package read_test
 
 import (
-	"os"
-	"path"
-
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
@@ -13,7 +10,7 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-var _ = Describe("reading files", func() {
+var _ = Describe("Reading files", func() {
 
 	db := database.GetConnection()
 
@@ -21,7 +18,7 @@ var _ = Describe("reading files", func() {
 		setup.PrepareDatabaseForTesting()
 	})
 
-	Describe("reading file info", func() {
+	Describe("ReadMultipleFiles", func() {
 		var results []*types.Song
 		var files chan *types.File
 
@@ -103,83 +100,4 @@ var _ = Describe("reading files", func() {
 			})
 		})
 	})
-
-	Describe("scanning a directory recursively", func() {
-		var results []*types.File
-
-		var testScanDirectory = func() {
-			results = nil
-			files := read.ScanDirectory(read.TestDirectory)
-
-			done := false
-
-			for !done {
-				select {
-				case result, more := <-files:
-					if more {
-						results = append(results, result)
-					}
-					done = !more
-				}
-			}
-		}
-
-		Context("when the database is empty", func() {
-			BeforeEach(testScanDirectory)
-
-			It("should return a channel with all the files in the directory", func() {
-				Expect(results).To(HaveLen(2))
-
-				if results[0].RelativePath == read.TestSong.RelativePath {
-					Expect(results[0].RelativePath).To(Equal(read.TestSong.RelativePath))
-					Expect(results[1].RelativePath).To(Equal(read.TestSongNested.RelativePath))
-				} else {
-					Expect(results[1].RelativePath).To(Equal(read.TestSong.RelativePath))
-					Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
-				}
-			})
-		})
-
-		Context("when the database already contains one of the files", func() {
-			BeforeEach(func() {
-				info, _ := os.Stat(path.Join(read.TestSong.BasePath, read.TestSong.RelativePath))
-
-				db.MustExec(
-					`
-					insert into songs (title, artist, album, base_path, relative_path, modified_date)
-					values ($1, $2, $3, $4, $5, $6)
-					`,
-					"old title",
-					"old artist",
-					"old album",
-					read.TestSong.BasePath,
-					read.TestSong.RelativePath,
-					info.ModTime().Unix(),
-				)
-
-				testScanDirectory()
-			})
-
-			It("should only return those files which do not exist in the database", func() {
-				Expect(results).To(HaveLen(1))
-				Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
-			})
-		})
-
-		Context("when an error previously occurred scanning one of the files", func() {
-			BeforeEach(func() {
-				db.MustExec(`
-				insert into scan_errors (base_path, relative_path, error)
-				values ($1, $2, $3)
-				`, read.TestSong.BasePath, read.TestSong.RelativePath, "A bad thing happened")
-
-				testScanDirectory()
-			})
-
-			It("should only return those files which did not have errors marked against them", func() {
-				Expect(results).To(HaveLen(1))
-				Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
-			})
-		})
-	})
 })

+ 29 - 7
gmus-backend/pkg/services/scanner.go → gmus-backend/pkg/read/scanner.go

@@ -1,17 +1,39 @@
-package services
+package read
+
+// Scan library directory all at once
 
 import (
 	"github.com/felamaslen/gmus-backend/pkg/config"
 	"github.com/felamaslen/gmus-backend/pkg/database"
 	"github.com/felamaslen/gmus-backend/pkg/logger"
-	"github.com/felamaslen/gmus-backend/pkg/read"
 	"github.com/felamaslen/gmus-backend/pkg/repository"
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-const LOG_EVERY = 100
-
-const BATCH_SIZE = 100
+func ScanDirectory(directory string) chan *types.File {
+	db := database.GetConnection()
+	l := logger.CreateLogger(config.GetConfig().LogLevel)
+
+	filteredOutput := make(chan *types.File)
+	allFiles := make(chan *types.File)
+
+	go func() {
+		batchFilterFiles(db, l, &filteredOutput, &allFiles, directory)
+	}()
+
+	go func() {
+		recursiveDirScan(
+			db,
+			l,
+			&allFiles,
+			directory,
+			"",
+			true,
+		)
+	}()
+
+	return filteredOutput
+}
 
 func UpsertSongsFromChannel(songs chan *types.Song) {
 	var l = logger.CreateLogger(config.GetConfig().LogLevel)
@@ -64,10 +86,10 @@ func ScanAndInsert(musicDirectory string) {
 	var l = logger.CreateLogger(config.GetConfig().LogLevel)
 
 	l.Info("Scanning directory for files...\n")
-	files := read.ScanDirectory(musicDirectory)
+	files := ScanDirectory(musicDirectory)
 
 	l.Info("Reading files...\n")
-	songs := read.ReadMultipleFiles(musicDirectory, files)
+	songs := ReadMultipleFiles(musicDirectory, files)
 
 	l.Info("Inserting data...\n")
 	UpsertSongsFromChannel(songs)

+ 86 - 5
gmus-backend/pkg/services/scanner_test.go → gmus-backend/pkg/read/scanner_test.go

@@ -1,23 +1,104 @@
-package services_test
+package read_test
 
 import (
+	"os"
+	"path"
+
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
 	"github.com/felamaslen/gmus-backend/pkg/database"
 	"github.com/felamaslen/gmus-backend/pkg/read"
-	"github.com/felamaslen/gmus-backend/pkg/services"
 	setup "github.com/felamaslen/gmus-backend/pkg/testing"
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-var _ = Describe("Music scanner service", func() {
+var _ = Describe("Scanning directories", func() {
 	db := database.GetConnection()
 
 	BeforeEach(func() {
 		setup.PrepareDatabaseForTesting()
 	})
 
+	Describe("ScanDirectory", func() {
+		var results []*types.File
+
+		var testScanDirectory = func() {
+			results = nil
+			files := read.ScanDirectory(read.TestDirectory)
+
+			done := false
+
+			for !done {
+				select {
+				case result, more := <-files:
+					if more {
+						results = append(results, result)
+					}
+					done = !more
+				}
+			}
+		}
+
+		Context("when the database is empty", func() {
+			BeforeEach(testScanDirectory)
+
+			It("should return a channel with all the files in the directory", func() {
+				Expect(results).To(HaveLen(2))
+
+				if results[0].RelativePath == read.TestSong.RelativePath {
+					Expect(results[0].RelativePath).To(Equal(read.TestSong.RelativePath))
+					Expect(results[1].RelativePath).To(Equal(read.TestSongNested.RelativePath))
+				} else {
+					Expect(results[1].RelativePath).To(Equal(read.TestSong.RelativePath))
+					Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
+				}
+			})
+		})
+
+		Context("when the database already contains one of the files", func() {
+			BeforeEach(func() {
+				info, _ := os.Stat(path.Join(read.TestSong.BasePath, read.TestSong.RelativePath))
+
+				db.MustExec(
+					`
+					insert into songs (title, artist, album, base_path, relative_path, modified_date)
+					values ($1, $2, $3, $4, $5, $6)
+					`,
+					"old title",
+					"old artist",
+					"old album",
+					read.TestSong.BasePath,
+					read.TestSong.RelativePath,
+					info.ModTime().Unix(),
+				)
+
+				testScanDirectory()
+			})
+
+			It("should only return those files which do not exist in the database", func() {
+				Expect(results).To(HaveLen(1))
+				Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
+			})
+		})
+
+		Context("when an error previously occurred scanning one of the files", func() {
+			BeforeEach(func() {
+				db.MustExec(`
+				insert into scan_errors (base_path, relative_path, error)
+				values ($1, $2, $3)
+				`, read.TestSong.BasePath, read.TestSong.RelativePath, "A bad thing happened")
+
+				testScanDirectory()
+			})
+
+			It("should only return those files which did not have errors marked against them", func() {
+				Expect(results).To(HaveLen(1))
+				Expect(results[0].RelativePath).To(Equal(read.TestSongNested.RelativePath))
+			})
+		})
+	})
+
 	Describe("UpsertSongsFromChannel", func() {
 		var songs chan *types.Song
 
@@ -49,7 +130,7 @@ var _ = Describe("Music scanner service", func() {
 				}
 			}()
 
-			services.UpsertSongsFromChannel(songs)
+			read.UpsertSongsFromChannel(songs)
 		}
 
 		Context("when the songs do not already exist in the database", func() {
@@ -153,7 +234,7 @@ var _ = Describe("Music scanner service", func() {
 
 	Describe("ScanAndInsert", func() {
 		It("should recursively scan files from a directory and add them to the database", func() {
-			services.ScanAndInsert(read.TestDirectory)
+			read.ScanAndInsert(read.TestDirectory)
 
 			var songs []types.Song
 			err := db.Select(&songs, `

+ 162 - 0
gmus-backend/pkg/read/watcher.go

@@ -0,0 +1,162 @@
+package read
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/felamaslen/gmus-backend/pkg/database"
+	"github.com/felamaslen/gmus-backend/pkg/logger"
+	"github.com/felamaslen/gmus-backend/pkg/repository"
+	"github.com/felamaslen/gmus-backend/pkg/types"
+	"github.com/fsnotify/fsnotify"
+	"github.com/jmoiron/sqlx"
+)
+
+// Watch file system for real time updates
+
+var watcher *fsnotify.Watcher
+
+func watchDir(path string, fi os.FileInfo, err error) error {
+	if fi.Mode().IsDir() {
+		return watcher.Add(path)
+	}
+	return nil
+}
+
+func getRelativePath(fileName string, rootDirectory string) (relativePath string, err error) {
+	if strings.Index(fileName, rootDirectory) != 0 {
+		return "", fmt.Errorf("File is not in root directory")
+	}
+	if len(fileName) < len(rootDirectory)+1 {
+		return "", fmt.Errorf("File is empty apart from root directory")
+	}
+	relativePath = (fileName[len(rootDirectory)+1:])
+	return
+}
+
+func handleFileRemoveOrRename(l *logger.Logger, db *sqlx.DB, rootDirectory string, event fsnotify.Event) {
+	relativePath, err := getRelativePath(event.Name, rootDirectory)
+	if err != nil {
+		l.Warn("[WATCH] delete error: %v\n", err)
+		return
+	}
+
+	l.Verbose("[WATCH] delete: basePath=%s, relativePath=%s\n", rootDirectory, relativePath)
+	watcher.Remove(event.Name)
+	repository.DeleteSongByPath(db, rootDirectory, relativePath)
+	return
+}
+
+// This is an ugly hack to wait until a file has completed writing, since fsnotify
+// doesn't support the IN_CLOSE_WRITE event
+// See this and related issues: https://github.com/fsnotify/fsnotify/pull/313, https://github.com/fsnotify/fsnotify/issues/22
+const WRITE_WAIT_TIME = 2 * time.Second
+
+var writeWaitMap = map[string]*time.Timer{}
+
+func handleFileOnceWritten(l *logger.Logger, db *sqlx.DB, rootDirectory string, filePath string) {
+	relativePath, err := getRelativePath(filePath, rootDirectory)
+	l.Verbose("[WATCH] create: basePath=%s, relativePath=%s\n", rootDirectory, relativePath)
+
+	if err != nil {
+		l.Warn("[WATCH] invalid path: %v\n", err)
+		return
+	}
+
+	file, err := os.Stat(filePath)
+	if err != nil {
+		l.Warn("[WATCH] error reading file: %v\n", err)
+		return
+	}
+	if file.IsDir() {
+		return
+	}
+
+	validFile := GetFileInfo(file, relativePath)
+	if validFile == nil {
+		l.Warn("[WATCH] invalid file: %s\n", filePath)
+		return
+	}
+	song, err := ReadFile(rootDirectory, validFile)
+	if err != nil {
+		l.Warn("[WATCH] error scanning file (file: %v): %v\n", validFile, err)
+		return
+	}
+
+	var batch [BATCH_SIZE]*types.Song
+	batch[0] = song
+	if err := repository.BatchUpsertSongs(db, &batch, 1); err != nil {
+		l.Error("[WATCH] error adding file: %v\n", err)
+	}
+}
+
+func waitUntilFileIsWritten(l *logger.Logger, db *sqlx.DB, rootDirectory string, event fsnotify.Event) {
+	var filePath = event.Name
+
+	if writeWaitMap[filePath] != nil {
+		writeWaitMap[filePath].Stop()
+	}
+	writeWaitMap[filePath] = time.NewTimer(WRITE_WAIT_TIME)
+
+	go func() {
+		<-writeWaitMap[filePath].C
+		handleFileOnceWritten(l, db, rootDirectory, filePath)
+		delete(writeWaitMap, filePath)
+	}()
+}
+
+func handleFileCreateEvent(l *logger.Logger, db *sqlx.DB, rootDirectory string, event fsnotify.Event) {
+	filePath := event.Name
+	file, err := os.Stat(filePath)
+	if err != nil {
+		l.Warn("[WATCH] error reading file: %v\n", err)
+		return
+	}
+	if file.IsDir() {
+		l.Verbose("[WATCH] adding directory to watcher: %s\n", filePath)
+		watcher.Add(filePath)
+	} else {
+		waitUntilFileIsWritten(l, db, rootDirectory, event)
+	}
+}
+
+func handleWatcherEvent(l *logger.Logger, db *sqlx.DB, rootDirectory string, event fsnotify.Event) {
+	switch event.Op {
+	case fsnotify.Remove:
+		handleFileRemoveOrRename(l, db, rootDirectory, event)
+	case fsnotify.Rename:
+		handleFileRemoveOrRename(l, db, rootDirectory, event)
+	case fsnotify.Create:
+		handleFileCreateEvent(l, db, rootDirectory, event)
+	case fsnotify.Write:
+		waitUntilFileIsWritten(l, db, rootDirectory, event)
+	}
+}
+
+func WatchLibraryRecursive(l *logger.Logger, rootDirectory string) {
+	watcher, _ = fsnotify.NewWatcher()
+	defer watcher.Close()
+
+	if err := filepath.Walk(rootDirectory, watchDir); err != nil {
+		l.Error("[WATCH] walk error: %s\n", err.Error())
+	}
+
+	db := database.GetConnection()
+
+	done := make(chan bool)
+	go func() {
+		for {
+			select {
+			case event := <-watcher.Events:
+				handleWatcherEvent(l, db, rootDirectory, event)
+			case err := <-watcher.Errors:
+				l.Error("[WATCH] error: %v\n", err)
+			}
+		}
+	}()
+
+	<-done
+}

+ 3 - 0
gmus-backend/pkg/repository/README.md

@@ -0,0 +1,3 @@
+# repository
+
+This package solely contains database logic.

+ 5 - 0
gmus-backend/pkg/repository/queries.go

@@ -80,6 +80,11 @@ set
   ,modified_date = excluded.modified_date
 `
 
+const queryDeleteSongByPath = `
+delete from songs
+where base_path = $1 and relative_path = $2
+`
+
 const querySelectNextSong = `
 select
   s1.id

+ 1 - 1
gmus-backend/pkg/repository/scan_test.go

@@ -12,7 +12,7 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-var _ = Describe("scan repository", func() {
+var _ = Describe("Scan repository", func() {
 	db := database.GetConnection()
 
 	BeforeEach(func() {

+ 10 - 0
gmus-backend/pkg/repository/songs.go

@@ -99,3 +99,13 @@ func BatchUpsertSongs(db *sqlx.DB, batch *[BATCH_SIZE]*types.Song, batchSize int
 
 	return err
 }
+
+func DeleteSongByPath(db *sqlx.DB, basePath string, relativePath string) (count int64, err error) {
+	result, err := db.Exec(queryDeleteSongByPath, basePath, relativePath)
+	if err != nil {
+		count = 0
+		return
+	}
+	count, err = result.RowsAffected()
+	return
+}

+ 28 - 1
gmus-backend/pkg/repository/songs_test.go

@@ -10,7 +10,7 @@ import (
 	"github.com/felamaslen/gmus-backend/pkg/types"
 )
 
-var _ = Describe("songs repository", func() {
+var _ = Describe("Songs repository", func() {
 	db := database.GetConnection()
 
 	BeforeEach(func() {
@@ -177,4 +177,31 @@ var _ = Describe("songs repository", func() {
 			})
 		})
 	})
+
+	Describe("DeleteSongByPath", func() {
+		BeforeEach(func() {
+			_, err := db.Query(`
+			insert into songs (title, artist, album, duration, base_path, relative_path, modified_date, track_number)
+			values ('Title', 'Artist', 'Album', 123, '/base/path', 'path/to/file.ogg', 10123123, 1)
+			`)
+			if err != nil {
+				panic(err)
+			}
+		})
+
+		It("should delete a song from the database matching the given path", func() {
+			var songs []*types.Song
+
+			db.Select(&songs, "select * from songs where base_path = $1 and relative_path = $2", "/base/path", "path/to/file.ogg")
+			Expect(songs).To(HaveLen(1))
+			Expect(songs[0].Id).NotTo(BeZero())
+
+			songs = []*types.Song{}
+
+			repository.DeleteSongByPath(db, "/base/path", "path/to/file.ogg")
+
+			db.Select(&songs, "select * from songs where base_path = $1 and relative_path = $2", "/base/path", "path/to/file.ogg")
+			Expect(songs).To(HaveLen(0))
+		})
+	})
 })

+ 1 - 1
gmus-backend/pkg/server/actions_test.go

@@ -33,7 +33,7 @@ var _ = Describe("Server actions", func() {
 	})
 
 	Describe("PublishActionFromClient", func() {
-		Describe("state set actions", func() {
+		Describe("State set actions", func() {
 			Context("when the action is valid", func() {
 				var action server.Action
 				json.Unmarshal([]byte(actionStateSetValid), &action)

+ 20 - 0
gmus-backend/pkg/server/fetch.go

@@ -17,8 +17,28 @@ type ArtistsResponse struct {
 	Artists []string `json:"artists"`
 }
 
+func GetPagedArtists(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
+}
+
 func routeFetchArtists(l *logger.Logger, rdb redis.Cmdable, w http.ResponseWriter, r *http.Request) error {
 	db := database.GetConnection()
+	// This returns all artists for now.
+	// TODO: add a query option which uses the above GetPagedArtists function to enable paging
 	artists, err := repository.SelectAllArtists(db)
 	if err != nil {
 		return err

+ 10 - 10
gmus-backend/pkg/services/fetch_test.go → gmus-backend/pkg/server/fetch_test.go

@@ -1,4 +1,4 @@
-package services_test
+package server_test
 
 import (
 	"fmt"
@@ -8,18 +8,18 @@ import (
 	. "github.com/onsi/gomega"
 
 	"github.com/felamaslen/gmus-backend/pkg/database"
-	"github.com/felamaslen/gmus-backend/pkg/services"
+	"github.com/felamaslen/gmus-backend/pkg/server"
 	setup "github.com/felamaslen/gmus-backend/pkg/testing"
 )
 
-var _ = Describe("Fetching data", func() {
+var _ = Describe("Fetching functions", func() {
 	db := database.GetConnection()
 
 	BeforeEach(func() {
 		setup.PrepareDatabaseForTesting()
 	})
 
-	Describe("getArtists", func() {
+	Describe("GetPagedArtists", func() {
 		var insertArtists = func(artists []string) {
 			var trackNumbers = make([]int, len(artists))
 			var titles = make([]string, len(artists))
@@ -75,7 +75,7 @@ var _ = Describe("Fetching data", func() {
 
 		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)
+				artists, more := server.GetPagedArtists(100, 0)
 
 				Expect(*artists).To(HaveLen(0))
 				Expect(more).To(BeFalse())
@@ -88,7 +88,7 @@ var _ = Describe("Fetching data", func() {
 			})
 
 			It("should return an empty string", func() {
-				artists, more := services.GetArtists(100, 0)
+				artists, more := server.GetPagedArtists(100, 0)
 
 				Expect(*artists).To(HaveLen(1))
 				Expect((*artists)[0]).To(Equal(""))
@@ -102,7 +102,7 @@ var _ = Describe("Fetching data", func() {
 			})
 
 			It("should return an ordered set matching the limit", func() {
-				artists, _ := services.GetArtists(3, 0)
+				artists, _ := server.GetPagedArtists(3, 0)
 
 				Expect(*artists).To(HaveLen(3))
 
@@ -112,21 +112,21 @@ var _ = Describe("Fetching data", func() {
 			})
 
 			It("should set more to true", func() {
-				_, more := services.GetArtists(3, 0)
+				_, more := server.GetPagedArtists(3, 0)
 
 				Expect(more).To(BeTrue())
 			})
 
 			Context("when paging", func() {
 				It("should return the next set of results", func() {
-					artists, _ := services.GetArtists(3, 1)
+					artists, _ := server.GetPagedArtists(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)
+					_, more := server.GetPagedArtists(3, 1)
 
 					Expect(more).To(BeFalse())
 				})

+ 8 - 0
gmus-backend/pkg/server/server.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/felamaslen/gmus-backend/pkg/config"
 	"github.com/felamaslen/gmus-backend/pkg/logger"
+	"github.com/felamaslen/gmus-backend/pkg/read"
 	"github.com/go-redis/redis"
 	"github.com/gorilla/mux"
 	"github.com/rs/cors"
@@ -19,6 +20,13 @@ func StartServer() {
 	rdb := redis.NewClient(&redis.Options{Addr: conf.RedisUrl})
 	defer rdb.Close()
 
+	if conf.LibraryWatch {
+		l.Info("Watching library for changes")
+		go read.WatchLibraryRecursive(l, conf.LibraryDirectory)
+	} else {
+		l.Verbose("Not watching library for changes")
+	}
+
 	router := mux.NewRouter()
 
 	healthRoutes(l, router)

+ 0 - 24
gmus-backend/pkg/services/fetch.go

@@ -1,24 +0,0 @@
-package services
-
-import (
-	"github.com/felamaslen/gmus-backend/pkg/database"
-	"github.com/felamaslen/gmus-backend/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
-}

+ 0 - 13
gmus-backend/pkg/services/services_suite_test.go

@@ -1,13 +0,0 @@
-package services_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestRepository(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Services Suite")
-}