func (fs *fsOrphanScan) Scan(baseFolder string, subFolder string, orphanCancelChan chan struct{}) (int, error) {
	if baseFolder == "" && subFolder == "" {
		return 0, errors.New("orphan scan: no base folder or subfolder")
	}
	return fsSource.OrphanScan(baseFolder, subFolder, fs.verbose, orphanCancelChan)
}
Scan scans for media files which have been removed from the media directory, and removes
them as appropriate.  An orphan is defined as follows:
- Art: art file is no longer present in the filesystem
- Artist: no more songs contain this artist's ID
- Album: no more songs contain this album's ID
- Folder: folder no longer present in the filesystem, or folder contains no items
- Song: song is no longer present in the filesystem
The baseFolder is the root location of the media folder.  As wavepipe currently supports only
one media folder, any media which does not reside in this folder is orphaned.  If left blank,
only the subFolder will be checked.
The subFolder is the current file location, under the baseFolder.  This is used to allow for
quick scans of a small subsection of the directory, such as on a filesystem change.  Any files
which are in the database, but do not exist on disk, will be orphaned and removed.
package core
import (
	"errors"
	"log"
	"time"
	"github.com/mdlayher/wavepipe/common"
	"github.com/mdlayher/wavepipe/env"
	"github.com/mdlayher/goset"
	"github.com/romanoff/fsmonitor"
)
var artSet = set.New(".jpg", ".jpeg", ".png")
var mediaSet = set.New(".ape", ".flac", ".m4a", ".mp3", ".mpc", ".ogg", ".wma", ".wv")
var fsQueue = make(chan fsTask, 10)
var fsSource fileSource
type fsTask interface {
	Folders() (string, string)
	SetFolders(string, string)
	Scan(string, string, chan struct{}) (int, error)
	Verbose(bool)
}
type fileSource interface {
	MediaScan(string, bool, chan struct{}) (int, error)
	OrphanScan(string, string, bool, chan struct{}) (int, error)
}
func fsManager(mediaFolder string, fsKillChan chan struct{}) {
	log.Println("fs: starting...")
	cancelQueue := make(chan chan struct{}, 10)
	fsSource = fsFileSource{}
	if env.IsTest() {
		fsSource = memFileSource{}
	}
	fsTaskCount := 0
	watcherChan := make(chan struct{})
	o := new(fsOrphanScan)
	o.SetFolders(mediaFolder, "")
	o.Verbose(true)
	fsQueue <- o
	m := new(fsMediaScan)
	m.SetFolders(mediaFolder, "")
	m.Verbose(true)
	fsQueue <- m
	go func() {
		for {
			select {
			case task := <-fsQueue:
				cancelChan := make(chan struct{})
				cancelQueue <- cancelChan
				baseFolder, subFolder := task.Folders()
				changes, err := task.Scan(baseFolder, subFolder, cancelChan)
				if err != nil {
					log.Println(err)
				}
				if changes > 0 {
					common.UpdateScanTime()
				}
				cancelChan = <-cancelQueue
				close(cancelChan)
				fsTaskCount++
				if fsTaskCount == 2 {
					close(watcherChan)
				}
			}
		}
	}()
	go func() {
		<-watcherChan
		watcher, err := fsmonitor.NewWatcher()
		if err != nil {
			log.Println(err)
			return
		}
		go func() {
			recentModifySet := set.New()
			recentRenameSet := set.New()
			for {
				select {
				case ev := <-watcher.Event:
					switch {
					case ev.IsModify():
						if !recentModifySet.Add(ev.Name) {
							break
						}
						go func() {
							<-time.After(2 * time.Second)
							recentModifySet.Remove(ev.Name)
						}()
						fallthrough
					case ev.IsCreate():
						<-time.After(250 * time.Millisecond)
						m := new(fsMediaScan)
						m.SetFolders(ev.Name, "")
						m.Verbose(false)
						fsQueue <- m
					case ev.IsRename():
						if !recentRenameSet.Add(ev.Name) {
							break
						}
						go func() {
							<-time.After(2 * time.Second)
							recentRenameSet.Remove(ev.Name)
						}()
						fallthrough
					case ev.IsDelete():
						<-time.After(250 * time.Millisecond)
						o := new(fsOrphanScan)
						o.SetFolders("", ev.Name)
						o.Verbose(false)
						fsQueue <- o
					}
				case err := <-watcher.Error:
					log.Println(err)
					return
				}
			}
		}()
		if err := watcher.Watch(mediaFolder); err != nil {
			log.Println(err)
			return
		}
		log.Println("fs: watching folder:", mediaFolder)
	}()
	for {
		select {
		case <-fsKillChan:
			log.Println("fs: halting tasks")
			for i := 0; i < len(cancelQueue); i++ {
				f := <-cancelQueue
				if f == nil {
					continue
				}
				f <- struct{}{}
				log.Println("fs: task halted")
			}
			log.Println("fs: stopped!")
			fsKillChan <- struct{}{}
			return
		}
	}
}
type fsMediaScan struct {
	baseFolder string
	subFolder  string
	verbose    bool
}
func (fs *fsMediaScan) Folders() (string, string) {
	return fs.baseFolder, fs.subFolder
}
func (fs *fsMediaScan) SetFolders(baseFolder string, subFolder string) {
	fs.baseFolder = baseFolder
	fs.subFolder = subFolder
}
func (fs *fsMediaScan) Verbose(verbose bool) {
	fs.verbose = verbose
}
func (fs *fsMediaScan) Scan(mediaFolder string, subFolder string, walkCancelChan chan struct{}) (int, error) {
	if subFolder != "" {
		return 0, errors.New("media scan: subfolder not valid for media scan operation")
	}
	return fsSource.MediaScan(mediaFolder, fs.verbose, walkCancelChan)
}
type fsOrphanScan struct {
	baseFolder string
	subFolder  string
	verbose    bool
}
func (fs *fsOrphanScan) Folders() (string, string) {
	return fs.baseFolder, fs.subFolder
}
func (fs *fsOrphanScan) SetFolders(baseFolder string, subFolder string) {
	fs.baseFolder = baseFolder
	fs.subFolder = subFolder
}
func (fs *fsOrphanScan) Verbose(verbose bool) {
	fs.verbose = verbose
}