From 342928d608736d5086afdda10e8bb67578d2aba2 Mon Sep 17 00:00:00 2001 From: sentriz Date: Tue, 7 May 2019 14:10:47 +0100 Subject: [PATCH] clean scan --- cmd/scanner/main.go | 102 ++++++++++++++++++------------- db/model.go | 11 ++-- handler/handler_sub_by_folder.go | 33 +++++++++- subsonic/media.go | 10 +-- 4 files changed, 102 insertions(+), 54 deletions(-) diff --git a/cmd/scanner/main.go b/cmd/scanner/main.go index f225601..d8a0b82 100644 --- a/cmd/scanner/main.go +++ b/cmd/scanner/main.go @@ -1,3 +1,14 @@ +// this scanner tries to scan with a single unsorted walk of the music +// directory - which means you can come across the cover of an album/folder +// before the tracks (and therefore the album) which is an issue because +// when inserting into the album table, we need a reference to the cover. +// to solve this we're using godirwalk's PostChildrenCallback and some +// globals. +// +// Album -> needs a CoverID +// Folder -> needs a CoverID +// -> needs a ParentID + package main import ( @@ -5,8 +16,6 @@ import ( "log" "os" "path" - "path/filepath" - "strings" "time" "github.com/dhowden/tag" @@ -21,7 +30,7 @@ var ( orm *gorm.DB tx *gorm.DB // seenTracks is used to keep every track we've seen so that - // we can later remove old tracks from the database + // we can remove old tracks in the clean up stage seenTracks = make(map[string]bool) // seenDirs is used for inserting to the folders table (for browsing // by folders instead of tags) which helps us work out a folder's @@ -29,11 +38,6 @@ var ( seenDirs = make(dirStack, 0) ) -func isCover(filename string) bool { - _, ok := coverFilenames[strings.ToLower(filename)] - return ok -} - func readTags(fullPath string) (tag.Metadata, error) { trackData, err := os.Open(fullPath) if err != nil { @@ -47,19 +51,33 @@ func readTags(fullPath string) (tag.Metadata, error) { return tags, nil } -// handleFolder is for browse by folders, while handleFile is for both -func handleFolder(fullPath string, info *godirwalk.Dirent) error { - stat, err := os.Stat(fullPath) - if err != nil { - return fmt.Errorf("when stating folder: %v", err) +func handleCover(fullPath string, stat os.FileInfo) error { + modTime := stat.ModTime() + cover := db.Cover{ + Path: fullPath, } + err := tx.Where(cover).First(&cover).Error + if !gorm.IsRecordNotFoundError(err) && + modTime.Before(cover.UpdatedAt) { + return nil + } + cover.AlbumID = 0 + cover.FolderID = seenDirs.Peek() + tx.Save(&cover) + return nil +} + +// handleFolder is for browse by folders, while handleTrack is for both +func handleFolder(fullPath string, stat os.FileInfo) error { + // this must be run before any tracks so that seenDirs is + // correct for the coming tracks modTime := stat.ModTime() folder := db.Folder{ Path: fullPath, } // skip if the record exists and hasn't been modified since // the last scan - err = tx.Where(folder).First(&folder).Error + err := tx.Where(folder).First(&folder).Error if !gorm.IsRecordNotFoundError(err) && modTime.Before(folder.UpdatedAt) { // even though we don't want to update this record, @@ -77,23 +95,7 @@ func handleFolder(fullPath string, info *godirwalk.Dirent) error { return nil } -func handleFile(fullPath string, info *godirwalk.Dirent) error { - stat, err := os.Stat(fullPath) - if err != nil { - return fmt.Errorf("when stating file: %v", err) - } - modTime := stat.ModTime() - _, filename := path.Split(fullPath) - if isCover(filename) { - return nil - } - longExt := filepath.Ext(filename) - extension := strings.ToLower(longExt[1:]) - // check if us audio and save mime type for later - mime, ok := audioExtensions[extension] - if !ok { - return nil - } +func handleTrack(fullPath string, stat os.FileInfo, mime, exten string) error { // add the full path to the seen set. see the comment above // seenTracks for more seenTracks[fullPath] = true @@ -101,9 +103,10 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error { track := db.Track{ Path: fullPath, } + modTime := stat.ModTime() // skip if the record exists and hasn't been modified since // the last scan - err = tx.Where(track).First(&track).Error + err := tx.Where(track).First(&track).Error if !gorm.IsRecordNotFoundError(err) && modTime.Before(track.UpdatedAt) { return nil @@ -122,11 +125,11 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error { track.TotalTracks = totalTracks track.TrackNumber = trackNumber track.Year = tags.Year() - track.Suffix = extension + track.Suffix = exten track.ContentType = mime track.Size = int(stat.Size()) track.FolderID = seenDirs.Peek() - // set album artist { + // albumArtist := db.AlbumArtist{ Name: tags.AlbumArtist(), } @@ -136,7 +139,7 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error { tx.Save(&albumArtist) } track.AlbumArtistID = albumArtist.ID - // set album + // album := db.Album{ AlbumArtistID: albumArtist.ID, Title: tags.Album(), @@ -148,25 +151,36 @@ func handleFile(fullPath string, info *godirwalk.Dirent) error { tx.Save(&album) } track.AlbumID = album.ID - // save track + // tx.Save(&track) return nil } +func handleItem(fullPath string, info *godirwalk.Dirent) error { + fmt.Println(fullPath) + return nil + stat, err := os.Stat(fullPath) + if err != nil { + return fmt.Errorf("error stating: %v", err) + } + if info.IsDir() { + return handleFolder(fullPath, stat) + } + if isCover(fullPath) { + return handleCover(fullPath, stat) + } + if mime, exten, ok := isAudio(fullPath); ok { + return handleTrack(fullPath, stat, mime, exten) + } + return nil +} + func handleFolderCompletion(fullPath string, info *godirwalk.Dirent) error { seenDirs.Pop() log.Printf("processed folder `%s`\n", fullPath) return nil } -func handleItem(fullPath string, info *godirwalk.Dirent) error { - // TODO: stat here instead of in each handler - if info.IsDir() { - return handleFolder(fullPath, info) - } - return handleFile(fullPath, info) -} - func createDatabase() { tx.AutoMigrate( &db.Album{}, diff --git a/db/model.go b/db/model.go index e32209f..c764d21 100644 --- a/db/model.go +++ b/db/model.go @@ -47,11 +47,14 @@ type Track struct { // Cover represents the covers table type Cover struct { + IDBase CrudBase - AlbumID int `gorm:"primary_key;auto_increment:false"` - Album Album - Image []byte - Path string `gorm:"not null;unique_index"` + AlbumID int `gorm:"index"` + Album Album + FolderID int `gorm:"index"` + Folder Folder + Image []byte + Path string `gorm:"not null;unique_index"` } // User represents the users table diff --git a/handler/handler_sub_by_folder.go b/handler/handler_sub_by_folder.go index 04d2a4b..5ae7197 100644 --- a/handler/handler_sub_by_folder.go +++ b/handler/handler_sub_by_folder.go @@ -31,9 +31,40 @@ func (c *Controller) GetIndexes(w http.ResponseWriter, r *http.Request) { }) } sub := subsonic.NewResponse() - sub.Artists = indexes + sub.Indexes = &subsonic.Indexes{ + LastModified: 0, + Index: indexes, + } respond(w, r, sub) } func (c *Controller) GetMusicDirectory(w http.ResponseWriter, r *http.Request) { + id, err := getIntParam(r, "id") + if err != nil { + respondError(w, r, 10, "please provide an `id` parameter") + return + } + var folders []*db.Folder + c.DB.Where("parent_id = ?", id).Find(&folders) + if len(folders) == 0 { + respondError(w, r, 40, "couldn't find any directories") + return + } + var cFolder db.Folder + c.DB.First(&cFolder, id) + sub := subsonic.NewResponse() + sub.Directory = &subsonic.Directory{ + ID: cFolder.ID, + Parent: cFolder.ParentID, + Name: cFolder.Name, + } + for _, folder := range folders { + sub.Directory.Children = append(sub.Directory.Children, &subsonic.Child{ + Parent: cFolder.ID, + ID: folder.ID, + Title: folder.Name, + IsDir: true, + }) + } + respond(w, r, sub) } diff --git a/subsonic/media.go b/subsonic/media.go index d30ad8a..8224c65 100644 --- a/subsonic/media.go +++ b/subsonic/media.go @@ -60,11 +60,11 @@ type Index struct { } type Directory struct { - ID int `xml:"id,attr" json:"id"` - Parent int `xml:"parent,attr" json:"parent"` - Name string `xml:"name,attr" json:"name"` - Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` - Children []Child `xml:"child" json:"child"` + ID int `xml:"id,attr" json:"id"` + Parent int `xml:"parent,attr" json:"parent"` + Name string `xml:"name,attr" json:"name"` + Starred string `xml:"starred,attr,omitempty" json:"starred,omitempty"` + Children []*Child `xml:"child" json:"child"` } type Child struct {