package syncer import ( "bufio" "fmt" "os" "path/filepath" "strings" ) var playlistExtensions = map[string]bool{ ".m3u": true, ".m3u8": true, } // findPlaylistFile looks for a single .m3u/.m3u8 file directly inside folder. // Returns "" if none is present. Returns an error if more than one is found, // since the order would then be ambiguous. func findPlaylistFile(folder string, entries []os.DirEntry) (string, error) { var found []string for _, e := range entries { if e.IsDir() { continue } if playlistExtensions[strings.ToLower(filepath.Ext(e.Name()))] { found = append(found, e.Name()) } } switch len(found) { case 0: return "", nil case 1: return filepath.Join(folder, found[0]), nil default: return "", fmt.Errorf("multiple playlist files found in %s (%s) - keep only one to define the chapter order", folder, strings.Join(found, ", ")) } } // parseM3U reads an m3u/m3u8 playlist and returns the referenced entry paths // in file order. Blank lines and lines starting with "#" (comments/EXTM3U // directives) are ignored. Entries are returned exactly as written in the // file (relative or absolute); resolution against the playlist's folder // happens in the caller. func parseM3U(path string) ([]string, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("reading playlist %s: %w", path, err) } defer f.Close() var entries []string scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } entries = append(entries, line) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("reading playlist %s: %w", path, err) } return entries, nil } // orderTracksByPlaylist reorders tracks according to the entry order in the // given m3u file. Tracks not referenced by the playlist are appended // afterwards in their existing (alphabetical) order, so nothing is silently // dropped from the sync. Playlist entries that don't match any local track // are ignored. func orderTracksByPlaylist(folder, playlistPath string, tracks []Track) ([]Track, error) { entries, err := parseM3U(playlistPath) if err != nil { return nil, err } byPath := make(map[string]Track, len(tracks)) for _, t := range tracks { byPath[t.Path] = t } used := make(map[string]bool, len(entries)) ordered := make([]Track, 0, len(tracks)) for _, entry := range entries { resolved := entry if !filepath.IsAbs(resolved) { resolved = filepath.Join(folder, resolved) } resolved = filepath.Clean(resolved) if t, ok := byPath[resolved]; ok { ordered = append(ordered, t) used[resolved] = true } } // Append any local tracks not referenced by the playlist, preserving // their existing order, so they aren't silently excluded from sync. for _, t := range tracks { if !used[t.Path] { ordered = append(ordered, t) } } return ordered, nil }