From 5ec5fc86ab917e79b86ba616b9df8e7ba6670e0b Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Thu, 12 Feb 2026 18:17:37 +0800 Subject: [PATCH] fix(mediascanner): isolate SkipFilesystemScan scanners to prevent file wipe SkipFilesystemScan launchers (e.g. RetroBat, Kodi) generate results independently and don't filter the shared file list. Previously, the scanner loop used a pipeline where each scanner's output replaced the shared files slice. When an independent scanner ignored its input (like RetroBat's when gamelist.xml is missing), it wiped all files found by other launchers or the filesystem walk. Now SkipFilesystemScan scanners receive nil input and their results are appended, while non-skip scanners retain the existing pipeline behavior. --- pkg/database/mediascanner/mediascanner.go | 52 ++- .../mediascanner/mediascanner_test.go | 412 ++++++++++++++++++ pkg/helpers/launchers_test.go | 31 ++ pkg/helpers/paths.go | 11 + pkg/helpers/paths_test.go | 4 - 5 files changed, 495 insertions(+), 15 deletions(-) diff --git a/pkg/database/mediascanner/mediascanner.go b/pkg/database/mediascanner/mediascanner.go index 696f1a65..f1f1dd99 100644 --- a/pkg/database/mediascanner/mediascanner.go +++ b/pkg/database/mediascanner/mediascanner.go @@ -217,6 +217,10 @@ func GetSystemPaths( for i := range launchers { // Skip filesystem scanning for launchers that don't need it if launchers[i].SkipFilesystemScan { + log.Trace(). + Str("launcher", launchers[i].ID). + Str("system", system.ID). + Msg("skipping filesystem scan for launcher") continue } for _, folder := range launchers[i].Folders { @@ -226,6 +230,13 @@ func GetSystemPaths( } } + log.Trace(). + Str("system", system.ID). + Int("launchers", len(launchers)). + Strs("folders", folders). + Strs("rootFolders", validRootFolders). + Msg("resolving system paths") + // check for / for _, gf := range validRootFolders { for _, folder := range folders { @@ -463,6 +474,14 @@ func GetFiles( return nil, fmt.Errorf("failed to walk directory %s: %w", realPath, err) } walkElapsed := time.Since(walkStartTime) + + results, err := stack.get() + if err != nil { + return nil, err + } + + allResults = append(allResults, *results...) + log.Debug(). Str("system", systemID). Str("path", realPath). @@ -488,13 +507,6 @@ func GetFiles( Msg("directory walk took longer than expected - large directory or slow storage") } - results, err := stack.get() - if err != nil { - return nil, err - } - - allResults = append(allResults, *results...) - // change root back to symlink if realPath != path { for i := range allResults { @@ -918,17 +930,35 @@ func NewNamesIndex( } } - // for each system launcher in a platform, run the results through its - // custom scan function if one exists + // Run each system launcher's custom scanner if one exists. + // + // SkipFilesystemScan launchers (e.g. RetroBat, Kodi) generate results + // independently — they don't filter/enrich the shared file list, so they + // receive empty input and their results are *appended* to files. This + // prevents an independent scanner that ignores its input from wiping out + // files discovered by other launchers or the filesystem walk. + // + // Non-skip launchers (e.g. Batocera gamelist.xml enrichment) act as a + // pipeline: they receive the current files and their output *replaces* + // files, allowing them to filter, reorder, or add metadata. launchers := helpers.GlobalLauncherCache.GetLaunchersBySystem(k) for i := range launchers { l := &launchers[i] if l.Scanner != nil { log.Debug().Msgf("running %s scanner for system: %s", l.ID, systemID) var scanErr error - files, scanErr = l.Scanner(ctx, cfg, systemID, files) + if l.SkipFilesystemScan { + // Isolated: scanner gets empty input, results accumulated + var independent []platforms.ScanResult + independent, scanErr = l.Scanner(ctx, cfg, systemID, nil) + if scanErr == nil { + files = append(files, independent...) + } + } else { + // Pipeline: scanner filters/enriches existing files + files, scanErr = l.Scanner(ctx, cfg, systemID, files) + } if scanErr != nil { - // Check if this is a cancellation error if errors.Is(scanErr, context.Canceled) { return handleCancellationWithRollback(ctx, db, "Media indexing cancelled during custom scanner") } diff --git a/pkg/database/mediascanner/mediascanner_test.go b/pkg/database/mediascanner/mediascanner_test.go index 74cd2bd2..1bf1e3be 100644 --- a/pkg/database/mediascanner/mediascanner_test.go +++ b/pkg/database/mediascanner/mediascanner_test.go @@ -1244,6 +1244,418 @@ func TestAnyScannerProgressUpdates(t *testing.T) { } } +// TestGetSystemPaths_CustomLauncherAbsolutePaths tests that GetSystemPaths returns +// valid paths for a custom launcher with absolute Folders pointing to real temp directories. +func TestGetSystemPaths_CustomLauncherAbsolutePaths(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directories to simulate custom launcher media dirs + ps2Dir := t.TempDir() + + // Create test files in the directory + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "game2.iso"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path Folders + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetSystemPaths with no root folders (custom launchers use absolute paths) + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + results := GetSystemPaths(cfg, platform, []string{}, systems) + + // The custom launcher's absolute folder should be resolved + require.Len(t, results, 1, "Should find exactly one path for PS2 custom launcher") + assert.Equal(t, systemdefs.SystemPS2, results[0].System.ID) + assert.Equal(t, ps2Dir, results[0].Path) +} + +// TestGetFiles_CustomLauncherMatchesFiles is the critical reproduction test: +// it verifies that GetFiles() actually finds and matches files when walking +// a directory from a custom launcher with absolute paths. +func TestGetFiles_CustomLauncherMatchesFiles(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files + ps2Dir := t.TempDir() + testFiles := []string{"Final Fantasy X.iso", "Metal Gear Solid 3.iso", "Shadow of the Colossus.bin"} + for _, f := range testFiles { + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, f), []byte("test"), 0o600)) + } + // Also create a file that should NOT match + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "readme.txt"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform - RootDirs is needed by PathIsLauncher for root-relative folder matching + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetFiles with the custom launcher's directory + ctx := context.Background() + files, err := GetFiles(ctx, cfg, platform, systemdefs.SystemPS2, ps2Dir) + require.NoError(t, err) + + // Should find all matching files (.iso and .bin) but not .txt + assert.Len(t, files, 3, "Should find 3 matching files (.iso and .bin)") + + // Verify the right files were found + foundFiles := make(map[string]bool) + for _, f := range files { + foundFiles[filepath.Base(f)] = true + } + assert.True(t, foundFiles["Final Fantasy X.iso"]) + assert.True(t, foundFiles["Metal Gear Solid 3.iso"]) + assert.True(t, foundFiles["Shadow of the Colossus.bin"]) + assert.False(t, foundFiles["readme.txt"], "Non-matching extension should be excluded") +} + +// TestGetFiles_CustomLauncherNestedDirectories verifies that GetFiles walks +// subdirectories within a custom launcher's absolute path. +func TestGetFiles_CustomLauncherNestedDirectories(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with nested structure + ps2Dir := t.TempDir() + rpgDir := filepath.Join(ps2Dir, "RPG") + actionDir := filepath.Join(ps2Dir, "Action") + require.NoError(t, os.MkdirAll(rpgDir, 0o750)) + require.NoError(t, os.MkdirAll(actionDir, 0o750)) + + require.NoError(t, os.WriteFile(filepath.Join(rpgDir, "FFX.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(actionDir, "DMC3.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "root_game.iso"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetFiles + ctx := context.Background() + files, err := GetFiles(ctx, cfg, platform, systemdefs.SystemPS2, ps2Dir) + require.NoError(t, err) + + assert.Len(t, files, 3, "Should find files in root and nested directories") +} + +// TestNewNamesIndex_CustomLauncherE2E is a full end-to-end test: +// custom launcher → GetSystemPaths → GetFiles → AddMediaPath → verify DB has entries. +func TestNewNamesIndex_CustomLauncherE2E(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files + ps2Dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "Final Fantasy X.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "Metal Gear Solid 3.bin"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2-e2e", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + assert.Equal(t, 2, filesIndexed, "Should have indexed 2 files from custom launcher") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 2, "Database should contain 2 media entries for PS2") + + // Verify the paths are correct + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["Final Fantasy X.iso"], "Should find Final Fantasy X.iso in DB") + assert.True(t, foundPaths["Metal Gear Solid 3.bin"], "Should find Metal Gear Solid 3.bin in DB") +} + +// TestNewNamesIndex_MixedSkipFilesystemScanAndCustomLauncher tests the scenario where +// a SkipFilesystemScan launcher (e.g., RetroBat) AND a custom launcher both target +// the same system (e.g., PS2). Verifies the custom launcher's files still get indexed. +func TestNewNamesIndex_MixedSkipFilesystemScanAndCustomLauncher(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files for the custom launcher + customDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game2.mdf"), []byte("test"), 0o600)) + + // SkipFilesystemScan launcher with a Scanner that ignores its input and + // returns empty — simulates RetroBat when gamelist.xml is missing. + skipLauncher := platforms.Launcher{ + ID: "retrobat-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{"ps2"}, + Extensions: []string{".iso", ".bin", ".chd"}, + SkipFilesystemScan: true, + Scanner: func(_ context.Context, _ *config.Instance, _ string, + _ []platforms.ScanResult, + ) ([]platforms.ScanResult, error) { + return nil, nil + }, + } + + // Custom launcher with absolute path and additional extensions + customLauncher := platforms.Launcher{ + ID: "custom-ps2-emulator", + SystemID: systemdefs.SystemPS2, + Folders: []string{customDir}, + Extensions: []string{".iso", ".mdf"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return( + []platforms.Launcher{skipLauncher, customLauncher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + // The custom launcher's files should be indexed even though + // the SkipFilesystemScan launcher also targets PS2 + assert.Equal(t, 2, filesIndexed, "Should have indexed 2 files from custom launcher") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 2, "Database should contain 2 media entries for PS2") + + // Verify both file types were indexed + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["game1.iso"], "Should find .iso file in DB") + assert.True(t, foundPaths["game2.mdf"], "Should find .mdf file in DB") +} + +// TestNewNamesIndex_IndependentScannerDoesNotWipeFiles is the critical regression +// test for the scanner isolation fix. A custom launcher provides filesystem files, +// and a SkipFilesystemScan launcher has a Scanner that ignores its input and returns +// its own results. Both launchers' files must end up in the DB. +func TestNewNamesIndex_IndependentScannerDoesNotWipeFiles(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files for the custom (filesystem) launcher + customDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game2.bin"), []byte("test"), 0o600)) + + // SkipFilesystemScan launcher whose Scanner ignores input and generates its own + // results (e.g. Kodi API, gamelist.xml with independent paths). + skipLauncher := platforms.Launcher{ + ID: "independent-scanner", + SystemID: systemdefs.SystemPS2, + SkipFilesystemScan: true, + Scanner: func(_ context.Context, _ *config.Instance, _ string, + _ []platforms.ScanResult, + ) ([]platforms.ScanResult, error) { + return []platforms.ScanResult{ + {Name: "Scanner Game A", Path: "/virtual/scanner-game-a.iso"}, + {Name: "Scanner Game B", Path: "/virtual/scanner-game-b.iso"}, + }, nil + }, + } + + // Custom launcher with real filesystem files + customLauncher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{customDir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return( + []platforms.Launcher{skipLauncher, customLauncher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + // Both launchers' files should be indexed: 2 from filesystem + 2 from scanner + assert.Equal(t, 4, filesIndexed, + "Should have indexed 4 files total (2 filesystem + 2 from independent scanner)") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 4, "Database should contain 4 media entries for PS2") + + // Verify all files are present + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["game1.iso"], "Should find filesystem game1.iso in DB") + assert.True(t, foundPaths["game2.bin"], "Should find filesystem game2.bin in DB") + assert.True(t, foundPaths["scanner-game-a.iso"], "Should find scanner game A in DB") + assert.True(t, foundPaths["scanner-game-b.iso"], "Should find scanner game B in DB") +} + // TestZaparooignoreMarker tests that directories containing a .zaparooignore file // are skipped during media scanning along with all their subdirectories. func TestZaparooignoreMarker(t *testing.T) { diff --git a/pkg/helpers/launchers_test.go b/pkg/helpers/launchers_test.go index 611cb163..9122a7ed 100644 --- a/pkg/helpers/launchers_test.go +++ b/pkg/helpers/launchers_test.go @@ -26,6 +26,7 @@ import ( "github.com/ZaparooProject/zaparoo-core/v2/pkg/platforms" "github.com/ZaparooProject/zaparoo-core/v2/pkg/testing/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseLifecycle(t *testing.T) { @@ -214,6 +215,36 @@ func TestParseCustomLaunchers_AllUnknownSystems(t *testing.T) { assert.Empty(t, launchers) } +func TestParseCustomLaunchers_AbsolutePathWithSystemID(t *testing.T) { + t.Parallel() + + mockPlatform := mocks.NewMockPlatform() + mockPlatform.On("ID").Return("test") + + customLaunchers := []config.LaunchersCustom{ + { + ID: "ps2-custom", + System: "PS2", + Execute: "pcsx2 [[media_path]]", + MediaDirs: []string{"/emulation/roms/ps2", "/mnt/games/ps2"}, + FileExts: []string{"iso", ".bin", "MDF", " .chd "}, + }, + } + + launchers := ParseCustomLaunchers(mockPlatform, customLaunchers) + + require.Len(t, launchers, 1) + l := launchers[0] + + assert.Equal(t, "ps2-custom", l.ID) + assert.Equal(t, "PS2", l.SystemID, "SystemID should be the canonical ID from LookupSystem") + assert.Equal(t, []string{"/emulation/roms/ps2", "/mnt/games/ps2"}, l.Folders, + "MediaDirs should map directly to Folders") + assert.Equal(t, []string{".iso", ".bin", ".mdf", ".chd"}, l.Extensions, + "FileExts should be dot-prefixed and lowercased") + assert.NotNil(t, l.Launch, "Launch function should be set") +} + func TestFormatExtensions(t *testing.T) { t.Parallel() diff --git a/pkg/helpers/paths.go b/pkg/helpers/paths.go index 61d45222..64387c1d 100644 --- a/pkg/helpers/paths.go +++ b/pkg/helpers/paths.go @@ -139,6 +139,12 @@ func PathIsLauncher( } if !inRoot && !isAbs { + log.Trace(). + Str("launcher", l.ID). + Str("path", path). + Strs("folders", l.Folders). + Strs("rootDirs", rootDirs). + Msg("path not in any launcher folder (root or absolute)") return false } } @@ -154,6 +160,11 @@ func PathIsLauncher( if l.Test != nil { return l.Test(cfg, lp) } + log.Trace(). + Str("launcher", l.ID). + Str("path", path). + Strs("extensions", l.Extensions). + Msg("path extension did not match any launcher extension") return false } diff --git a/pkg/helpers/paths_test.go b/pkg/helpers/paths_test.go index 7ae9e5d2..8f0b22c1 100644 --- a/pkg/helpers/paths_test.go +++ b/pkg/helpers/paths_test.go @@ -527,10 +527,6 @@ func TestGetPathExt(t *testing.T) { } } -// Note: PathIsLauncher tests are in the integration tests (pkg/database/mediascanner) -// to avoid import cycles with the platforms package. The function is thoroughly tested -// through those integration tests which exercise all code paths. - // TestGetPathInfo_VirtualPathEdgeCases tests edge cases in GetPathInfo with virtual paths // that could cause issues with encoding/decoding, parsing, or display func TestGetPathInfo_VirtualPathEdgeCases(t *testing.T) {