fix(quick-19): multi-root path resolution for playlist M3U8 tracks
- Replace getLibraryRoot() with getAllLibraryRoots() returning all library paths - Add resolveM3UPath() for multi-root resolution with knownPaths lookup - Add toRelativePathMultiRoot() to save relative paths using correct root - Update removeM3UEntries, replaceM3UEntryPaths, findM3UEntry for []string roots - Update all 8 call sites in playlist.go for multi-root resolution - Update existing test signatures for new []string parameter types - Fix pre-existing golines formatting in scan_queue.go
This commit is contained in:
@@ -111,7 +111,9 @@ func (l *Library) SoftScanAllLibraries() error {
|
|||||||
// dynamic library_id unsupported by sqlc. No user input.
|
// dynamic library_id unsupported by sqlc. No user input.
|
||||||
for _, lib := range libs {
|
for _, lib := range libs {
|
||||||
result, claimErr := l.db.ExecContext(
|
result, claimErr := l.db.ExecContext(
|
||||||
`UPDATE audio_files SET library_id = ? WHERE library_id = 0 AND file_path LIKE ? || '%'`,
|
`UPDATE audio_files SET library_id = ?`+
|
||||||
|
` WHERE library_id = 0`+
|
||||||
|
` AND file_path LIKE ? || '%'`,
|
||||||
lib.ID, lib.Path+"/",
|
lib.ID, lib.Path+"/",
|
||||||
)
|
)
|
||||||
if claimErr != nil {
|
if claimErr != nil {
|
||||||
|
|||||||
+85
-13
@@ -370,6 +370,65 @@ func toRelativePath(absolutePath, libraryRoot string) string {
|
|||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveM3UPath resolves a relative M3U path against multiple
|
||||||
|
// library roots, returning the first absolute path that exists in
|
||||||
|
// the knownPaths set. If the path is already absolute and known,
|
||||||
|
// it is returned as-is. Falls back to the first root if no match
|
||||||
|
// is found, preserving current behavior for phantom tracks.
|
||||||
|
func resolveM3UPath(
|
||||||
|
relativePath string,
|
||||||
|
libraryRoots []string,
|
||||||
|
knownPaths map[string]struct{},
|
||||||
|
) string {
|
||||||
|
if filepath.IsAbs(relativePath) {
|
||||||
|
if _, ok := knownPaths[relativePath]; ok {
|
||||||
|
return relativePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativePath
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, root := range libraryRoots {
|
||||||
|
absPath := filepath.Join(root, relativePath)
|
||||||
|
if _, ok := knownPaths[absPath]; ok {
|
||||||
|
return absPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use first root (preserves current behavior
|
||||||
|
// for phantom tracks).
|
||||||
|
if len(libraryRoots) > 0 {
|
||||||
|
return filepath.Join(libraryRoots[0], relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// toRelativePathMultiRoot converts an absolute path to a relative
|
||||||
|
// path using the first library root that contains the path.
|
||||||
|
// If no root matches, the absolute path is returned unchanged.
|
||||||
|
func toRelativePathMultiRoot(
|
||||||
|
absolutePath string,
|
||||||
|
libraryRoots []string,
|
||||||
|
) string {
|
||||||
|
for _, root := range libraryRoots {
|
||||||
|
if root == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(root, absolutePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(rel, "..") {
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
// isValidM3UExtension checks whether a file extension is a
|
// isValidM3UExtension checks whether a file extension is a
|
||||||
// recognized M3U variant.
|
// recognized M3U variant.
|
||||||
func isValidM3UExtension(ext string) bool {
|
func isValidM3UExtension(ext string) bool {
|
||||||
@@ -414,17 +473,18 @@ func extractPlaylistID(filePath string) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// removeM3UEntries removes entries from a slice whose resolved
|
// removeM3UEntries removes entries from a slice whose resolved
|
||||||
// absolute paths appear in the target set.
|
// absolute paths appear in the target set. Each entry is resolved
|
||||||
|
// against all library roots.
|
||||||
func removeM3UEntries(
|
func removeM3UEntries(
|
||||||
entries []m3uEntry,
|
entries []m3uEntry,
|
||||||
targetAbsPaths map[string]struct{},
|
targetAbsPaths map[string]struct{},
|
||||||
libraryRoot string,
|
libraryRoots []string,
|
||||||
) []m3uEntry {
|
) []m3uEntry {
|
||||||
result := make([]m3uEntry, 0, len(entries))
|
result := make([]m3uEntry, 0, len(entries))
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
absPath := toAbsolutePath(
|
absPath := resolveM3UPath(
|
||||||
e.RelativePath, libraryRoot,
|
e.RelativePath, libraryRoots, targetAbsPaths,
|
||||||
)
|
)
|
||||||
if _, remove := targetAbsPaths[absPath]; remove {
|
if _, remove := targetAbsPaths[absPath]; remove {
|
||||||
continue
|
continue
|
||||||
@@ -438,19 +498,26 @@ func removeM3UEntries(
|
|||||||
|
|
||||||
// replaceM3UEntryPaths replaces the relative paths of entries
|
// replaceM3UEntryPaths replaces the relative paths of entries
|
||||||
// whose resolved absolute paths match keys in the replacements
|
// whose resolved absolute paths match keys in the replacements
|
||||||
// map. Values are new relative paths.
|
// map. Values are new relative paths. Each entry is resolved
|
||||||
|
// against all library roots.
|
||||||
func replaceM3UEntryPaths(
|
func replaceM3UEntryPaths(
|
||||||
entries []m3uEntry,
|
entries []m3uEntry,
|
||||||
replacements map[string]string,
|
replacements map[string]string,
|
||||||
libraryRoot string,
|
libraryRoots []string,
|
||||||
) []m3uEntry {
|
) []m3uEntry {
|
||||||
|
// Build a set of replacement keys for resolveM3UPath lookup.
|
||||||
|
keySet := make(map[string]struct{}, len(replacements))
|
||||||
|
for k := range replacements {
|
||||||
|
keySet[k] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
result := make([]m3uEntry, len(entries))
|
result := make([]m3uEntry, len(entries))
|
||||||
|
|
||||||
for i, e := range entries {
|
for i, e := range entries {
|
||||||
result[i] = e
|
result[i] = e
|
||||||
|
|
||||||
absPath := toAbsolutePath(
|
absPath := resolveM3UPath(
|
||||||
e.RelativePath, libraryRoot,
|
e.RelativePath, libraryRoots, keySet,
|
||||||
)
|
)
|
||||||
|
|
||||||
if newRel, ok := replacements[absPath]; ok {
|
if newRel, ok := replacements[absPath]; ok {
|
||||||
@@ -462,16 +529,21 @@ func replaceM3UEntryPaths(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findM3UEntry finds the M3U entry whose resolved absolute path
|
// findM3UEntry finds the M3U entry whose resolved absolute path
|
||||||
// matches the given target path. Returns the entry and its index,
|
// matches the given target path. Each entry is resolved against
|
||||||
// or -1 if not found.
|
// all library roots. Returns the entry and its index, or -1 if
|
||||||
|
// not found.
|
||||||
func findM3UEntry(
|
func findM3UEntry(
|
||||||
entries []m3uEntry,
|
entries []m3uEntry,
|
||||||
targetAbsPath string,
|
targetAbsPath string,
|
||||||
libraryRoot string,
|
libraryRoots []string,
|
||||||
) (m3uEntry, int) {
|
) (m3uEntry, int) {
|
||||||
|
targetSet := map[string]struct{}{
|
||||||
|
targetAbsPath: {},
|
||||||
|
}
|
||||||
|
|
||||||
for i, e := range entries {
|
for i, e := range entries {
|
||||||
absPath := toAbsolutePath(
|
absPath := resolveM3UPath(
|
||||||
e.RelativePath, libraryRoot,
|
e.RelativePath, libraryRoots, targetSet,
|
||||||
)
|
)
|
||||||
if absPath == targetAbsPath {
|
if absPath == targetAbsPath {
|
||||||
return e, i
|
return e, i
|
||||||
|
|||||||
@@ -731,7 +731,9 @@ func TestRemoveM3UEntries(t *testing.T) {
|
|||||||
"/music/Artist/Song2.flac": {},
|
"/music/Artist/Song2.flac": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := removeM3UEntries(entries, targets, "/music")
|
result := removeM3UEntries(
|
||||||
|
entries, targets, []string{"/music"},
|
||||||
|
)
|
||||||
|
|
||||||
if len(result) != 2 {
|
if len(result) != 2 {
|
||||||
t.Fatalf("expected 2 entries, got %d", len(result))
|
t.Fatalf("expected 2 entries, got %d", len(result))
|
||||||
@@ -765,7 +767,9 @@ func TestRemoveM3UEntriesAll(t *testing.T) {
|
|||||||
"/music/Song.flac": {},
|
"/music/Song.flac": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := removeM3UEntries(entries, targets, "/music")
|
result := removeM3UEntries(
|
||||||
|
entries, targets, []string{"/music"},
|
||||||
|
)
|
||||||
|
|
||||||
if len(result) != 0 {
|
if len(result) != 0 {
|
||||||
t.Errorf("expected 0 entries, got %d", len(result))
|
t.Errorf("expected 0 entries, got %d", len(result))
|
||||||
@@ -793,7 +797,7 @@ func TestReplaceM3UEntryPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := replaceM3UEntryPaths(
|
result := replaceM3UEntryPaths(
|
||||||
entries, replacements, "/music",
|
entries, replacements, []string{"/music"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(result) != 2 {
|
if len(result) != 2 {
|
||||||
@@ -836,7 +840,8 @@ func TestFindM3UEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry, idx := findM3UEntry(
|
entry, idx := findM3UEntry(
|
||||||
entries, "/music/Artist/Song2.flac", "/music",
|
entries, "/music/Artist/Song2.flac",
|
||||||
|
[]string{"/music"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if idx != 1 {
|
if idx != 1 {
|
||||||
@@ -853,7 +858,8 @@ func TestFindM3UEntry(t *testing.T) {
|
|||||||
|
|
||||||
// Not found.
|
// Not found.
|
||||||
_, idx = findM3UEntry(
|
_, idx = findM3UEntry(
|
||||||
entries, "/music/Artist/Missing.flac", "/music",
|
entries, "/music/Artist/Missing.flac",
|
||||||
|
[]string{"/music"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if idx != -1 {
|
if idx != -1 {
|
||||||
|
|||||||
+125
-54
@@ -367,12 +367,21 @@ func (s *Service) mergeTracksForPlaylist(
|
|||||||
return dbTracksToSlice(dbTracks)
|
return dbTracksToSlice(dbTracks)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
|
// Build a set of known DB paths for multi-root resolution.
|
||||||
|
knownPaths := make(
|
||||||
|
map[string]struct{}, len(dbTracks),
|
||||||
|
)
|
||||||
|
for k := range dbTracks {
|
||||||
|
knownPaths[k] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
tracks := make([]Track, 0, len(parsed.Entries))
|
tracks := make([]Track, 0, len(parsed.Entries))
|
||||||
|
|
||||||
for i, entry := range parsed.Entries {
|
for i, entry := range parsed.Entries {
|
||||||
absPath := toAbsolutePath(
|
absPath := resolveM3UPath(
|
||||||
entry.RelativePath, libraryRoot,
|
entry.RelativePath, libraryRoots, knownPaths,
|
||||||
)
|
)
|
||||||
|
|
||||||
if dbTrack, ok := dbTracks[absPath]; ok {
|
if dbTrack, ok := dbTracks[absPath]; ok {
|
||||||
@@ -841,7 +850,7 @@ func (s *Service) ImportPlaylist(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
resolved int
|
resolved int
|
||||||
@@ -850,14 +859,37 @@ func (s *Service) ImportPlaylist(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for _, entry := range parsed.Entries {
|
for _, entry := range parsed.Entries {
|
||||||
absPath := toAbsolutePath(
|
var audioFile sqlcgen.AudioFile
|
||||||
entry.RelativePath, libraryRoot,
|
|
||||||
)
|
|
||||||
|
|
||||||
audioFile, lookupErr := s.db.Queries.GetAudioFileByPath(
|
found := false
|
||||||
s.db.Ctx, absPath,
|
|
||||||
)
|
if filepath.IsAbs(entry.RelativePath) {
|
||||||
if lookupErr != nil {
|
af, err := s.db.Queries.GetAudioFileByPath(
|
||||||
|
s.db.Ctx, entry.RelativePath,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
audioFile = af
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, root := range libraryRoots {
|
||||||
|
absPath := toAbsolutePath(
|
||||||
|
entry.RelativePath, root,
|
||||||
|
)
|
||||||
|
|
||||||
|
af, err := s.db.Queries.GetAudioFileByPath(
|
||||||
|
s.db.Ctx, absPath,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
audioFile = af
|
||||||
|
found = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
// Track not in library — will appear as phantom.
|
// Track not in library — will appear as phantom.
|
||||||
unresolved++
|
unresolved++
|
||||||
|
|
||||||
@@ -876,7 +908,7 @@ func (s *Service) ImportPlaylist(
|
|||||||
s.logger.Warn(
|
s.logger.Warn(
|
||||||
"Could not add imported track",
|
"Could not add imported track",
|
||||||
"playlistId", created.ID,
|
"playlistId", created.ID,
|
||||||
"path", absPath,
|
"path", audioFile.FilePath,
|
||||||
"err", addErr,
|
"err", addErr,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -890,7 +922,7 @@ func (s *Service) ImportPlaylist(
|
|||||||
// Save the M3U8 file with entries (preserves unresolved
|
// Save the M3U8 file with entries (preserves unresolved
|
||||||
// paths for phantom display).
|
// paths for phantom display).
|
||||||
s.saveImportedPlaylistFile(
|
s.saveImportedPlaylistFile(
|
||||||
created.ID, playlistName, parsed.Entries, libraryRoot,
|
created.ID, playlistName, parsed.Entries, libraryRoots,
|
||||||
)
|
)
|
||||||
|
|
||||||
s.logger.Info(
|
s.logger.Info(
|
||||||
@@ -981,7 +1013,7 @@ func (s *Service) RestoreAllPlaylists() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
var totalRestored, totalUnresolved int
|
var totalRestored, totalUnresolved int
|
||||||
|
|
||||||
@@ -997,7 +1029,7 @@ func (s *Service) RestoreAllPlaylists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restored, unresolved := s.restoreSinglePlaylist(
|
restored, unresolved := s.restoreSinglePlaylist(
|
||||||
playlistID, file, libraryRoot,
|
playlistID, file, libraryRoots,
|
||||||
)
|
)
|
||||||
|
|
||||||
totalRestored += restored
|
totalRestored += restored
|
||||||
@@ -1014,11 +1046,12 @@ func (s *Service) RestoreAllPlaylists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// restoreSinglePlaylist restores tracks for a single playlist
|
// restoreSinglePlaylist restores tracks for a single playlist
|
||||||
// from its M3U8 file.
|
// from its M3U8 file. Each M3U8 entry is resolved against all
|
||||||
|
// library roots.
|
||||||
func (s *Service) restoreSinglePlaylist(
|
func (s *Service) restoreSinglePlaylist(
|
||||||
playlistID int64,
|
playlistID int64,
|
||||||
m3uPath string,
|
m3uPath string,
|
||||||
libraryRoot string,
|
libraryRoots []string,
|
||||||
) (restored, unresolved int) {
|
) (restored, unresolved int) {
|
||||||
parsed, err := parseM3U8(m3uPath)
|
parsed, err := parseM3U8(m3uPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1049,14 +1082,37 @@ func (s *Service) restoreSinglePlaylist(
|
|||||||
var position int
|
var position int
|
||||||
|
|
||||||
for _, entry := range parsed.Entries {
|
for _, entry := range parsed.Entries {
|
||||||
absPath := toAbsolutePath(
|
var audioFile sqlcgen.AudioFile
|
||||||
entry.RelativePath, libraryRoot,
|
|
||||||
)
|
|
||||||
|
|
||||||
audioFile, lookupErr := s.db.Queries.GetAudioFileByPath(
|
found := false
|
||||||
s.db.Ctx, absPath,
|
|
||||||
)
|
if filepath.IsAbs(entry.RelativePath) {
|
||||||
if lookupErr != nil {
|
af, lookupErr := s.db.Queries.GetAudioFileByPath(
|
||||||
|
s.db.Ctx, entry.RelativePath,
|
||||||
|
)
|
||||||
|
if lookupErr == nil {
|
||||||
|
audioFile = af
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, root := range libraryRoots {
|
||||||
|
absPath := toAbsolutePath(
|
||||||
|
entry.RelativePath, root,
|
||||||
|
)
|
||||||
|
|
||||||
|
af, lookupErr := s.db.Queries.GetAudioFileByPath(
|
||||||
|
s.db.Ctx, absPath,
|
||||||
|
)
|
||||||
|
if lookupErr == nil {
|
||||||
|
audioFile = af
|
||||||
|
found = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
unresolved++
|
unresolved++
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -1074,7 +1130,7 @@ func (s *Service) restoreSinglePlaylist(
|
|||||||
s.logger.Warn(
|
s.logger.Warn(
|
||||||
"Could not restore track",
|
"Could not restore track",
|
||||||
"playlistId", playlistID,
|
"playlistId", playlistID,
|
||||||
"path", absPath,
|
"path", audioFile.FilePath,
|
||||||
"err", addErr,
|
"err", addErr,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1169,24 +1225,29 @@ func (s *Service) playlistsDir() (string, error) {
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLibraryRoot returns the library root directory path.
|
// getAllLibraryRoots returns the root directory paths of all
|
||||||
// It first checks the legacy config DirectoryPath; if that is
|
// configured libraries. It first checks the legacy config
|
||||||
// empty (removed during multi-library migration) it falls back
|
// DirectoryPath; if that is set, it returns a single-element
|
||||||
// to the first library's path from the database.
|
// slice. Otherwise it queries all libraries from the database.
|
||||||
func (s *Service) getLibraryRoot() string {
|
func (s *Service) getAllLibraryRoots() []string {
|
||||||
|
// Legacy config fallback.
|
||||||
if s.libraryDir != nil {
|
if s.libraryDir != nil {
|
||||||
if dir := s.libraryDir.GetLibraryDirectory(); dir != "" {
|
if dir := s.libraryDir.GetLibraryDirectory(); dir != "" {
|
||||||
return dir
|
return []string{dir}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: query the first library from the database.
|
|
||||||
libs, err := s.db.Queries.GetAllLibraries(s.db.Ctx)
|
libs, err := s.db.Queries.GetAllLibraries(s.db.Ctx)
|
||||||
if err != nil || len(libs) == 0 {
|
if err != nil || len(libs) == 0 {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return libs[0].Path
|
roots := make([]string, len(libs))
|
||||||
|
for i, lib := range libs {
|
||||||
|
roots[i] = lib.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
// savePlaylistFile saves the current state of a playlist to its
|
// savePlaylistFile saves the current state of a playlist to its
|
||||||
@@ -1243,7 +1304,7 @@ func (s *Service) saveImportedPlaylistFile(
|
|||||||
playlistID int64,
|
playlistID int64,
|
||||||
name string,
|
name string,
|
||||||
entries []m3uEntry,
|
entries []m3uEntry,
|
||||||
libraryRoot string,
|
libraryRoots []string,
|
||||||
) {
|
) {
|
||||||
dir, err := s.playlistsDir()
|
dir, err := s.playlistsDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1255,16 +1316,22 @@ func (s *Service) saveImportedPlaylistFile(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert any absolute paths in entries to relative.
|
// Convert any absolute paths in entries to relative
|
||||||
|
// using the correct library root for each track.
|
||||||
converted := make([]m3uEntry, len(entries))
|
converted := make([]m3uEntry, len(entries))
|
||||||
|
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
|
// Resolve against all roots to get the absolute path,
|
||||||
|
// then convert back to relative using the matching root.
|
||||||
|
absPath := resolveM3UPath(
|
||||||
|
entry.RelativePath,
|
||||||
|
libraryRoots,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
converted[i] = m3uEntry{
|
converted[i] = m3uEntry{
|
||||||
RelativePath: toRelativePath(
|
RelativePath: toRelativePathMultiRoot(
|
||||||
toAbsolutePath(
|
absPath, libraryRoots,
|
||||||
entry.RelativePath, libraryRoot,
|
|
||||||
),
|
|
||||||
libraryRoot,
|
|
||||||
),
|
),
|
||||||
DurationSec: entry.DurationSec,
|
DurationSec: entry.DurationSec,
|
||||||
DisplayTitle: entry.DisplayTitle,
|
DisplayTitle: entry.DisplayTitle,
|
||||||
@@ -1301,7 +1368,7 @@ func (s *Service) buildM3UEntries(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
entries := make([]m3uEntry, 0, len(rows))
|
entries := make([]m3uEntry, 0, len(rows))
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
@@ -1310,8 +1377,8 @@ func (s *Service) buildM3UEntries(
|
|||||||
)
|
)
|
||||||
|
|
||||||
entries = append(entries, m3uEntry{
|
entries = append(entries, m3uEntry{
|
||||||
RelativePath: toRelativePath(
|
RelativePath: toRelativePathMultiRoot(
|
||||||
row.FilePath, libraryRoot,
|
row.FilePath, libraryRoots,
|
||||||
),
|
),
|
||||||
DurationSec: durationSec,
|
DurationSec: durationSec,
|
||||||
DisplayTitle: displayTitle(
|
DisplayTitle: displayTitle(
|
||||||
@@ -1445,7 +1512,7 @@ func (s *Service) FindPhantomMatches(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
// Load M3U8 entries for display title / duration data.
|
// Load M3U8 entries for display title / duration data.
|
||||||
m3uPath, err := findPlaylistFile(dir, playlistID)
|
m3uPath, err := findPlaylistFile(dir, playlistID)
|
||||||
@@ -1465,11 +1532,13 @@ func (s *Service) FindPhantomMatches(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build a lookup from absolute path to M3U entry.
|
// Build a lookup from absolute path to M3U entry.
|
||||||
|
// Use resolveM3UPath with a nil knownPaths to get the
|
||||||
|
// first-root fallback for each entry.
|
||||||
entryByPath := make(map[string]m3uEntry, len(entries))
|
entryByPath := make(map[string]m3uEntry, len(entries))
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
absPath := toAbsolutePath(
|
absPath := resolveM3UPath(
|
||||||
e.RelativePath, libraryRoot,
|
e.RelativePath, libraryRoots, nil,
|
||||||
)
|
)
|
||||||
entryByPath[absPath] = e
|
entryByPath[absPath] = e
|
||||||
}
|
}
|
||||||
@@ -1533,7 +1602,7 @@ func (s *Service) GetPhantomCandidates(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
// Find the M3U entry for this phantom.
|
// Find the M3U entry for this phantom.
|
||||||
m3uPath, err := findPlaylistFile(dir, playlistID)
|
m3uPath, err := findPlaylistFile(dir, playlistID)
|
||||||
@@ -1549,7 +1618,7 @@ func (s *Service) GetPhantomCandidates(
|
|||||||
parsed, parseErr := parseM3U8(m3uPath)
|
parsed, parseErr := parseM3U8(m3uPath)
|
||||||
if parseErr == nil {
|
if parseErr == nil {
|
||||||
entry, _ = findM3UEntry(
|
entry, _ = findM3UEntry(
|
||||||
parsed.Entries, phantomPath, libraryRoot,
|
parsed.Entries, phantomPath, libraryRoots,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1613,7 +1682,7 @@ func (s *Service) ResolvePhantomTracks(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
m3uPath, err := findPlaylistFile(dir, playlistID)
|
m3uPath, err := findPlaylistFile(dir, playlistID)
|
||||||
if err != nil || m3uPath == "" {
|
if err != nil || m3uPath == "" {
|
||||||
@@ -1681,7 +1750,9 @@ func (s *Service) ResolvePhantomTracks(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
newRel := toRelativePath(resolvedAbs, libraryRoot)
|
newRel := toRelativePathMultiRoot(
|
||||||
|
resolvedAbs, libraryRoots,
|
||||||
|
)
|
||||||
pathReplacements[phantomAbs] = newRel
|
pathReplacements[phantomAbs] = newRel
|
||||||
resolved++
|
resolved++
|
||||||
}
|
}
|
||||||
@@ -1689,7 +1760,7 @@ func (s *Service) ResolvePhantomTracks(
|
|||||||
// Rewrite the M3U8 with updated paths.
|
// Rewrite the M3U8 with updated paths.
|
||||||
if resolved > 0 {
|
if resolved > 0 {
|
||||||
updated := replaceM3UEntryPaths(
|
updated := replaceM3UEntryPaths(
|
||||||
parsed.Entries, pathReplacements, libraryRoot,
|
parsed.Entries, pathReplacements, libraryRoots,
|
||||||
)
|
)
|
||||||
|
|
||||||
playlist, nameErr := s.db.Queries.GetPlaylist(
|
playlist, nameErr := s.db.Queries.GetPlaylist(
|
||||||
@@ -1740,7 +1811,7 @@ func (s *Service) RemovePhantomTracks(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryRoot := s.getLibraryRoot()
|
libraryRoots := s.getAllLibraryRoots()
|
||||||
|
|
||||||
m3uPath, err := findPlaylistFile(dir, playlistID)
|
m3uPath, err := findPlaylistFile(dir, playlistID)
|
||||||
if err != nil || m3uPath == "" {
|
if err != nil || m3uPath == "" {
|
||||||
@@ -1766,7 +1837,7 @@ func (s *Service) RemovePhantomTracks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updated := removeM3UEntries(
|
updated := removeM3UEntries(
|
||||||
parsed.Entries, targetSet, libraryRoot,
|
parsed.Entries, targetSet, libraryRoots,
|
||||||
)
|
)
|
||||||
|
|
||||||
playlist, err := s.db.Queries.GetPlaylist(
|
playlist, err := s.db.Queries.GetPlaylist(
|
||||||
|
|||||||
Reference in New Issue
Block a user