// MediaScan scans for media files in the local filesystem func (fsFileSource) MediaScan(mediaFolder string, verbose bool, walkCancelChan chan struct{}) (int, error) { // Halt walk if needed var mutex sync.RWMutex haltWalk := false go func() { // Wait for signal <-walkCancelChan // Halt! mutex.Lock() haltWalk = true mutex.Unlock() }() // Track metrics about the walk artCount := 0 artistCount := 0 albumCount := 0 songCount := 0 songUpdateCount := 0 folderCount := 0 startTime := time.Now() // Track all folder IDs containing new art, and hold their art IDs artFiles := make([]folderArtPair, 0) // Cache entries which have been seen previously, to reduce database load folderCache := map[string]*data.Folder{} artistCache := map[string]*data.Artist{} albumCache := map[string]*data.Album{} if verbose { log.Println("fs: beginning media scan:", mediaFolder) } else { log.Println("fs: scanning:", mediaFolder) } // Invoke a recursive file walk on the given media folder, passing closure variables into // walkFunc to enable additional functionality err := filepath.Walk(mediaFolder, func(currPath string, info os.FileInfo, err error) error { // Stop walking immediately if needed mutex.RLock() if haltWalk { return errors.New("media scan: halted by channel") } mutex.RUnlock() // Make sure path is actually valid if info == nil { return errors.New("media scan: invalid path: " + currPath) } // Check for an existing folder for this item folder := new(data.Folder) if info.IsDir() { // If directory, use this path folder.Path = currPath } else { // If file, use the directory path folder.Path = path.Dir(currPath) } // Check for a cached folder, or attempt to load it if tempFolder, ok := folderCache[folder.Path]; ok { folder = tempFolder } else if err := folder.Load(); err != nil && err == sql.ErrNoRows { // Make sure items actually exist at this path files, err := ioutil.ReadDir(folder.Path) if err != nil { log.Println(err) return err } // No items, skip it if len(files) == 0 { return nil } // Set short title folder.Title = path.Base(folder.Path) // Check for a parent folder pFolder := new(data.Folder) // If scan is triggered by a file, we have to check the dir twice to get parent if info.IsDir() { pFolder.Path = path.Dir(currPath) } else { pFolder.Path = path.Dir(path.Dir(currPath)) } // Load parent if err := pFolder.Load(); err != nil && err != sql.ErrNoRows { log.Println(err) return err } // Copy parent folder's ID folder.ParentID = pFolder.ID // Save new folder if err := folder.Save(); err != nil { log.Println(err) return err } // Continue traversal folderCount++ } // Cache this folder folderCache[folder.Path] = folder // Check for a valid media or art extension ext := path.Ext(currPath) if !mediaSet.Has(ext) && !artSet.Has(ext) { return nil } // If item is art, check for existing art if artSet.Has(ext) { // Attempt to load existing art art := new(data.Art) art.FileName = currPath if err := art.Load(); err == sql.ErrNoRows { // On new art, capture art information from filesystem art.FileSize = info.Size() art.LastModified = info.ModTime().Unix() // Refuse to save a file with size 0, because the HTTP server will // not allow it to be sent with 0 Content-Length if art.FileSize == 0 { return nil } // Save new art if err := art.Save(); err != nil { log.Println(err) } else if err == nil { artCount++ // Add folder ID and to new art ID to slice artFiles = append(artFiles, folderArtPair{ folderID: folder.ID, artID: art.ID, }) } } // Continue to next file return nil } // Attempt to scan media file with taglib file, err := taglib.Read(currPath) if err != nil { log.Println(err) return fmt.Errorf("%s: %s", currPath, err.Error()) } // Generate a song model from the TagLib file song, err := data.SongFromFile(file) if err != nil { log.Println(err) return err } // Close file handle; no longer needed file.Close() // Populate filesystem-related struct fields using OS info song.FileName = currPath song.FileSize = info.Size() song.LastModified = info.ModTime().Unix() // Refuse to save a file with size 0, because the HTTP server will // not allow it to be sent with 0 Content-Length if song.FileSize == 0 { return nil } // Use this folder's ID song.FolderID = folder.ID // Check for a valid wavepipe file type integer ext = path.Ext(info.Name()) fileType, ok := data.FileTypeMap[ext] if !ok { return fmt.Errorf("fs: invalid file type: %s", ext) } song.FileTypeID = fileType // Generate an artist model from this song's metadata artist := data.ArtistFromSong(song) // Check for existing artist // Note: if the artist exists, this operation also loads necessary scanning information // such as their artist ID, for use in album and song generation if tempArtist, ok := artistCache[artist.Title]; ok { artist = tempArtist } else if err := artist.Load(); err == sql.ErrNoRows { // Save new artist if err := artist.Save(); err != nil { log.Println(err) } else if err == nil { log.Printf("Artist: [#%05d] %s", artist.ID, artist.Title) artistCount++ } } // Cache this artist artistCache[artist.Title] = artist // Generate the album model from this song's metadata album := data.AlbumFromSong(song) album.ArtistID = artist.ID // Generate cache key albumCacheKey := strconv.Itoa(album.ArtistID) + "_" + album.Title // Check for existing album // Note: if the album exists, this operation also loads necessary scanning information // such as the album ID, for use in song generation if tempAlbum, ok := albumCache[albumCacheKey]; ok { album = tempAlbum } else if err := album.Load(); err == sql.ErrNoRows { // Save album if err := album.Save(); err != nil { log.Println(err) } else if err == nil { log.Printf(" - Album: [#%05d] %s - %d - %s", album.ID, album.Artist, album.Year, album.Title) albumCount++ } } // Cache this album albumCache[albumCacheKey] = album // Add ID fields to song song.ArtistID = artist.ID song.AlbumID = album.ID // Make a duplicate song to check if song has been modified since last scan song2 := new(data.Song) song2.FileName = song.FileName // Check for existing song if err := song2.Load(); err == sql.ErrNoRows { // Save song (don't log these because they really slow things down) if err2 := song.Save(); err2 != nil && err2 != sql.ErrNoRows { log.Println(err2) } else if err2 == nil { songCount++ } } else { // Song already existed. Check if it's been updated if song.LastModified > song2.LastModified { // Update existing song song.ID = song2.ID if err2 := song.Update(); err2 != nil { log.Println(err2) } songUpdateCount++ } } // Successful media scan return nil }) // Check for filesystem walk errors if err != nil { return 0, err } // Iterate all new folder/art ID pairs for _, a := range artFiles { // Fetch all songs for the folder from the pair songs, err := data.DB.SongsForFolder(a.folderID) if err != nil { return 0, err } // Iterate and update songs with their new art ID for _, s := range songs { s.ArtID = a.artID if err := s.Update(); err != nil { return 0, err } } } // Print metrics if verbose { log.Printf("fs: media scan complete [time: %s]", time.Since(startTime).String()) log.Printf("fs: added: [art: %d] [artists: %d] [albums: %d] [songs: %d] [folders: %d]", artCount, artistCount, albumCount, songCount, folderCount) log.Printf("fs: updated: [songs: %d]", songUpdateCount) } // Sum up all changes sum := artCount + artistCount + albumCount + songCount + folderCount + songUpdateCount // No errors return sum, nil }
// MediaScan adds mock media files to the database from memory func (memFileSource) MediaScan(mediaFolder string, verbose bool, walkCancelChan chan struct{}) (int, error) { log.Println("mem: beginning mock media scan:", mediaFolder) // Iterate all media files and check for the matching prefix for _, song := range mockFiles { // Grab files with matching prefix if mediaFolder == song.FileName[0:len(mediaFolder)] { // Generate a folder model folder := new(data.Folder) folder.Path = "/mem" folder.Title = "/mem" // Attempt to load folder if err := folder.Load(); err == sql.ErrNoRows { // Save new folder if err := folder.Save(); err != nil { log.Println(err) } } // Generate an art model art := new(data.Art) art.FileName = "/mem/test.jpg" // Attempt to load art if err := art.Load(); err == sql.ErrNoRows { // Save new art if err := art.Save(); err != nil { log.Println(err) } } // Generate an artist model from this song's metadata artist := data.ArtistFromSong(&song) // Check for existing artist // Note: if the artist exists, this operation also loads necessary scanning information // such as their artist ID, for use in album and song generation if err := artist.Load(); err == sql.ErrNoRows { // Save new artist if err := artist.Save(); err != nil { log.Println(err) } } // Generate the album model from this song's metadata album := data.AlbumFromSong(&song) album.ArtistID = artist.ID // Check for existing album // Note: if the album exists, this operation also loads necessary scanning information // such as the album ID, for use in song generation if err := album.Load(); err == sql.ErrNoRows { // Save album if err := album.Save(); err != nil { log.Println(err) } } // Add ID fields to song song.ArtistID = artist.ID song.AlbumID = album.ID // Check for existing song if err := song.Load(); err == sql.ErrNoRows { // Save song (don't log these because they really slow things down) if err := song.Save(); err != nil { log.Println(err) } } } } log.Println("mem: mock media scan complete") return 0, nil }