repo_description: 🎧 Your Personal Streaming Service 
repo_topics: airsonic madsonic media-server music music-server navidrome opensubsonic raspberry-pi self-hosted streamer streaming-api streaming-audio subsonic subsonic-server
code: package cmd
  
  import (
  	"bufio"
  	"context"
  	"encoding/gob"
  	"fmt"
  	"os"
  	"strings"
  
  	"github.com/navidrome/navidrome/core"
  	"github.com/navidrome/navidrome/core/playlists"
  	"github.com/navidrome/navidrome/db"
  	"github.com/navidrome/navidrome/log"
  	"github.com/navidrome/navidrome/model"
  	"github.com/navidrome/navidrome/persistence"
  	"github.com/navidrome/navidrome/scanner"
  	"github.com/navidrome/navidrome/utils/pl"
  	"github.com/spf13/cobra"
  )
  
  var (
  	fullScan   bool
  	subprocess bool
  	targets    []string
  	targetFile string
  )
  
  func init() {
  	scanCmd.Flags().BoolVarP(&fullScan, "full", "f", false, "check all subfolders, ignoring timestamps")
  	scanCmd.Flags().BoolVarP(&subprocess, "subprocess", "", false, "run as subprocess (internal use)")
  	scanCmd.Flags().StringArrayVarP(&targets, "target", "t", []string{}, "list of libraryID:folderPath pairs, can be repeated (e.g., \"-t 1:Music/Rock -t 1:Music/Jazz -t 2:Classical\")")
  	scanCmd.Flags().StringVar(&targetFile, "target-file", "", "path to file containing targets (one libraryID:folderPath per line)")
  	rootCmd.AddCommand(scanCmd)
  }
  
  var scanCmd = &cobra.Command{
  	Use:   "scan",
  	Short: "Scan music folder",
  	Long:  "Scan music folder for updates",
  	Run: func(cmd *cobra.Command, args []string) {
  		runScanner(cmd.Context())
  	},
  }
  
  func trackScanInteractively(ctx context.Context, progress <-chan *scanner.ProgressInfo) {
  	for status := range pl.ReadOrDone(ctx, progress) {
  		if status.Warning != "" {
  			log.Warn(ctx, "Scan warning", "error", status.Warning)
  		}
  		if status.Error != "" {
  			log.Error(ctx, "Scan error", "error", status.Error)
  		}
  		// Discard the progress status, we only care about errors
  	}
  
  	if fullScan {
  		log.Info("Finished full rescan")
  	} else {
  		log.Info("Finished rescan")
  	}
  }
  
  func trackScanAsSubprocess(ctx context.Context, progress <-chan *scanner.ProgressInfo) {
  	encoder := gob.NewEncoder(os.Stdout)
  	for status := range pl.ReadOrDone(ctx, progress) {
  		err := encoder.Encode(status)
  		if err != nil {
  			log.Error(ctx, "Failed to encode status", err)
  		}
  	}
  }
  
  func runScanner(ctx context.Context) {
  	sqlDB := db.Db()
  	defer db.Db().Close()
  	ds := persistence.New(sqlDB)
  	pls := playlists.NewPlaylists(ds, core.NewImageUploadService())
  
  	// Parse targets from command line or file
  	var scanTargets []model.ScanTarget
  	var err error
  
  	if targetFile != "" {
  		scanTargets, err = readTargetsFromFile(targetFile)
  		if err != nil {
  			log.Fatal(ctx, "Failed to read targets from file", err)
  		}
  		log.Info(ctx, "Scanning specific folders from file", "numTargets", len(scanTargets))
  	} else if len(targets) > 0 {
  		scanTargets, err = model.ParseTargets(targets)
  		if err != nil {
  			log.Fatal(ctx, "Failed to parse targets", err)
  		}
  		log.Info(ctx, "Scanning specific folders", "numTargets", len(scanTargets))
  	}
  
  	progress, err := scanner.CallScan(ctx, ds, pls, fullScan, scanTargets)
  	if err != nil {
  		log.Fatal(ctx, "Failed to scan", err)
  	}
  
  	// Wait for the scanner to finish
  	if subprocess {
  		trackScanAsSubprocess(ctx, progress)
  	} else {
  		trackScanInteractively(ctx, progress)
  	}
  }
  
  // readTargetsFromFile reads scan targets from a file, one per line.
  // Each line should be in the format "libraryID:folderPath".
  // Empty lines and lines starting with # are ignored.
  func readTargetsFromFile(filePath string) ([]model.ScanTarget, error) {
  	file, err := os.Open(filePath)
  	if err != nil {
  		return nil, fmt.Errorf("failed to open target file: %w", err)
  	}
  	defer file.Close()
  
  	var targetStrings []string
  	scanner := bufio.NewScanner(file)
  	for scanner.Scan() {
  		line := strings.TrimSpace(scanner.Text())
  		// Skip empty lines and comments
  		if line == "" {
  			continue
  		}
  		targetStrings = append(targetStrings, line)
  	}
  
  	if err := scanner.Err(); err != nil {
  		return nil, fmt.Errorf("failed to read target file: %w", err)
  	}
  
  	return model.ParseTargets(targetStrings)
  }
  