Skip to content

Folder search permanently disables after a transient scan timeout (requires manual re-affirmation) #796

Open

Description

@AlySerry0

Summary

When a media-folder scan hits FOLDER_SEARCH_FIRST_FILE_TIMEOUT or FOLDER_SEARCH_TIMEOUT, FileSwitchManager._updateInfoThread sets folderSearchEnabled = False. That disables automatic file switching entirely until the user manually re-affirms their directories via File → Set Media Directories → OK. I'd like to propose making this self-healing: abort the slow pass, but keep the feature enabled so the next periodic pass can recover on its own.

Why the current behavior hurts

Timeouts here are usually transient. A network share or a spun-down external drive is slow on the first cold scan, but the very next periodic pass typically finishes in a couple of seconds once the OS/drive metadata caches are warm. Under today's behavior the first cold pass permanently disables the feature, so that fast warm pass never runs — the user is left with "not found" playlist items and a menu dance to recover.

This is a long-standing pain point — see #130 (same scenario: a Samba mount with thousands of files, asking for a bigger timeout).

To be upfront about root cause: in my case the real bottleneck was server-side (an NTFS-over-USB drive via ntfs-3g on a Raspberry Pi — cold directory enumeration ~30–50s, warm ~1–5s), and I fixed that on the server. I'm not asking Syncplay to make slow drives fast. This is purely about the client UX when a transient timeout does occur, which can affect anyone with a large or networked media directory.

Proposed behavior

  • On timeout, return from the pass (as today) but don't set folderSearchEnabled = False.
  • Surface the timeout notice only once (tracked with a folderSearchTimedOut flag) so it doesn't repeat every interval.
  • Clear the flag whenever a pass completes within the timeout, so it keeps self-healing and will still notify again on a future, distinct episode.

This keeps the original intent — the slow pass is still aborted, and the once-only notice plus the existing interval cadence keeps I/O bounded — while removing the permanent-disable and the manual re-affirmation requirement.

Follow-up: the folder-search-timeout-error message currently tells users to re-affirm via the menu, which would no longer be necessary. I'm happy to reword it (across locales) as part of the change.

Implementation

I have a small patch ready against master (client.py only, +17/−4):
master...AlySerry0:syncplay:folder-search-retry-not-disable

Happy to open a PR if you're open to this direction — or to adapt the recovery strategy to whatever you'd prefer (e.g. a backoff between retries after a timeout, or gating it behind a config option).

Environment

  • Syncplay 1.7.x, Windows 11
  • Media directories on a Samba/network share (alongside local drives)

Activity

  1. Et0h commented on Aug 17, 2026

    @Et0h
    Contributor

    Thanks for looking into this and trying to improve the folder search behaviour. I think this is of general interest to Syncplay users so want this fixed and fixed in a relatively robust/reliable way.

    I'm wondering if the main problem is slightly different from the one the current patch is addressing. At the moment Syncplay checks the media directories on a timer, so for a large folder or a network share it can end up doing quite a lot of repeated work even when nothing has changed.

    I think it might be better to use filesystem change notifications where we can, probably through watchdog, and only scan when Syncplay is told that something has changed. The latest watchdog needs Python 3.9+, but Python 3.8 is already EOL and we're moving the Windows build to Python 3.11+, so moving Syncplay's minimum to 3.9 for the next release seems reasonable to me.

    We would still need polling as a backup for filesystems where notifications aren't available or don't work reliably. In those cases the current 30 second interval could remain, while systems where notifications are working could just do an occasional full check as a safety net.

    I do agree with the point in #796 that a temporary timeout shouldn't permanently disable folder searching. If a backup scan is slow or times out, I think Syncplay should give one warning, leave folder searching enabled and try again later rather than repeatedly warning or requiring the user to set their media directories again. We should also make sure another scan isn't started while the previous one is still running (we already to do this but there are potential race conditions).

    As we're now trying to handle larger folders better, the current warning after only a couple of seconds also seems a bit quick. We could raise that default. Rather than add a whole config version system just for this, I think we can use the presence of the new folder monitoring options to tell whether an old config has already been updated. If the new option isn't there and the old value is still the old default, Syncplay can move it to the new default. If the old value has been changed by the user, it should keep that customised value instead. New configs would just get the new options and defaults from the start.

    If this sounds like a sensible direction to you then let me know and next time I have some free time I can have a look at putting together a patch for you to test against the setup which is causing the problem. Or if you would prefer you could have a go (using the Syncplay Code Contribution Markdown .md if you are making an AI-assisted PR) and I can provide feedback on your approach.

  2. AlySerry0 commented on Aug 17, 2026

    @AlySerry0
    Author

    Thanks — that direction makes sense to me, and I'm happy to have a go at it following the contribution guide (#763). Before I go deep, here's the plan plus a couple of questions so I build the right thing.

    Plan

    • Keep the timer poll as the always-on baseline (safety-net cadence) and layer a watchdog observer on top: when it reports a change under a media directory, trigger a scan; otherwise rely on an occasional full poll.
    • Timeout handling: one warning, leave folder search enabled, retry on the next interval, and clear the warned-state once a pass completes — i.e. the change already in this issue / my branch.
    • Guard against overlapping scans (single in-flight scan, and coalesce a burst of change events into one scan via a short debounce).
    • Raise the warning-threshold default, with migration keyed on the presence of the new option(s): if the new option is absent and the old value is still the old default, move it up; if the user has customised it, keep their value.
    • New options wired through config + CLI + GUI (GuiConfiguration.py) + console (consoleUI.py), with user-visible strings via getMessage().

    One honesty caveat
    watchdog leans on OS change notifications, which are unreliable/absent on SMB/CIFS network shares and FUSE mounts — which is my own setup (the drive that triggered this is a Samba share). So notifications should help local media directories a lot, but network-share users like me will still be served by the polling path. That's fine — it's exactly why the robust "poll + don't-disable-on-timeout + sane recovery" backbone matters regardless — I just wanted to flag it so we don't oversell notifications for the network case.

    Questions

    1. watchdog as a hard dependency in requirements, or optional with a graceful fallback to polling-only when it's missing/unavailable? I'd lean optional-with-fallback (keeps existing platforms and builds safe, and matches "stability first"), but happy to make it required if you'd prefer.
    2. Should this PR carry the Python 3.9 minimum bump and any build/CI changes, or would you rather handle the version/build plumbing on your side (since the Windows build is already moving to 3.11+) and I keep this PR to the folder-monitoring code?
    3. Any preferred naming for the new config option(s)? I'll otherwise propose something like folderSearchUseNotifications and go from there.

    I'll start scaffolding against master in the meantime.

  3. Et0h commented on Aug 17, 2026

    @Et0h
    Contributor

    Before we get too far into the exact spec, I think it would be useful to test one of the assumptions first.

    You mentioned that "watchdog" notifications are unreliable or unavailable for your Samba share. Since your setup is Windows 11, that may not necessarily be the case. On Windows, "watchdog" uses the native Windows directory change API, which in my limited understanding can work over SMB.

    Could you try this small standalone test against the same mapped drive or UNC path which is causing the problem? Ideally test both a change made on the Windows machine itself and a change made to the same share from another computer. The second case is the important one.

    import sys
    import time
    from datetime import datetime
    
    from watchdog.events import FileSystemEventHandler
    from watchdog.observers.read_directory_changes import WindowsApiObserver
    
    
    class PrintAllChanges(FileSystemEventHandler):
        def on_any_event(self, event):
            timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    
            line = "{}  {:10}  {}".format(
                timestamp,
                event.event_type,
                event.src_path
            )
    
            dest_path = getattr(event, "dest_path", None)
            if dest_path:
                line += " -> {}".format(dest_path)
    
            if event.is_directory:
                line += "  [directory]"
    
            print(line, flush=True)
    
    
    def main():
        if sys.platform != "win32":
            print("This test is intended for Windows.")
            return 1
    
        if len(sys.argv) != 2:
            print('Usage: python watch_test.py "Y:\\path"')
            print(r'   or: python watch_test.py "\\server\share\path"')
            return 1
    
        path = os.path.abspath(sys.argv[1])
    
        if not os.path.isdir(path):
            print("Directory does not exist or is not accessible:")
            print(path)
            return 1
    
        print("Watching:")
        print(path)
        print()
        print("Backend: WindowsApiObserver / ReadDirectoryChangesW")
        print("Test changes made locally and from another computer.")
        print("Press Ctrl+C to stop.")
        print()
    
        observer = WindowsApiObserver()
        observer.schedule(PrintAllChanges(), path, recursive=True)
    
        try:
            observer.start()
        except Exception as e:
            print("Could not start the Windows filesystem watcher:")
            print("{}: {}".format(type(e).__name__, e))
            return 1
    
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            print("\nStopping...")
        finally:
            observer.stop()
            observer.join()
    
        return 0
    
    
    if __name__ == "__main__":
        sys.exit(main())```
    
    Install "watchdog" with: ```py -m pip install watchdog```
    
    Then run it against the same path you use in Syncplay, for example: ```py .\watch_test.py "Y:\Media"```
    
    or directly against the UNC path: ```py .\watch_test.py "\\server\share\Media"```
    
    I'd then try creating, renaming and deleting a file locally, followed by doing the same thing from another computer connected to the share.
    
    If changes made from the other computer show up straight away, then your Samba use case may be able to use the notification path after all and we can simplify the fallback side of the design. If they don't show up, then we have a concrete case where polling is still needed and can design around the actual behaviour rather than assuming either way.
    
    This deliberately uses "WindowsApiObserver" rather than the generic "Observer", so we know the test is using the native Windows "ReadDirectoryChangesW" backend rather than accidentally falling back to polling.
    
  4. AlySerry0 commented on Aug 18, 2026

    @AlySerry0
    Author

    Thanks — I ran your test (using WindowsApiObserver explicitly, so this is ReadDirectoryChangesW and not an accidental polling fallback) against my Samba share (P: = \\serrypi.local\5TB, a Raspberry Pi; the drive is NTFS via ntfs-3g). The server has change notify = Yes and kernel change notify = Yes, and inotify does fire locally on the mount — so this isn't a misconfiguration.

    Watching a media root recursively, here is what the Windows client actually received (create / rename / delete, repeated):

    Change origin Top level of the watched folder Deep in an existing subfolder
    Local (this Windows PC writes to the share) delivered delivered
    Remote (change made on the Pi / another computer) delivered not delivered

    So local changes come through at any depth, but changes made from another machine only surface at the top level of the watched directory. A file dropped into an existing Show/Season/ folder by another computer produced no event at all.

    For file switching that's the awkward combination: the changes we'd most want to react to — someone else adding or moving media on the share — are exactly the ones that don't arrive when they're deep in the tree. So at least for a Samba share like this, the notification path can't replace polling; it can only augment it. That's why in the draft PR I kept polling always-on and made notifications purely additive rather than trimming the fallback.

    Happy to run any other variation you'd like (different depths, a non-recursive watch scheduled per-subfolder, or a native-Linux-filesystem share instead of ntfs-3g) if it would help pin the behaviour down.

  5. Et0h commented on Aug 18, 2026

    @Et0h
    Contributor

    Nice progress. The Windows 11 / Samba testing reported above was really useful in showing what does and doesn't work for your set-up. I don't use network shares so am somewhat dependent on you and others for testing and sense checking, but with that in mind here is my proposal for the next step:

    Looking at #130, #796 and the current #799 together, I think the best way forward is to keep the useful parts of this PR, particularly the transient-timeout fix, single-flight scan handling and watchdog support, but refine how notifications and polling work.

    Your Samba test shows that Windows notifications are useful but cannot be relied on recursively for all remotely-originated changes. I therefore agree that polling still needs to exist for correctness. However, I don't think that necessarily means keeping a full os.walk() every 30 seconds at all times.

    The approach I'd like to try is:

    • move watchdog lifecycle and watch bookkeeping into a small playlist-blind filemonitor.py helper rather than expanding client.py;
    • use received filesystem events directly to update mediaFilesCache where the event is unambiguous;
    • retain periodic reconciliation because some events can be missed;
    • poll at the existing roughly 30-second cadence when the current or next local playlist file cannot be resolved, but back off substantially while the files Syncplay needs are available;
    • on Windows network shares, supplement the recursive watch with a bounded number of direct non-recursive directory watches, prioritising directories relevant to the current/upcoming playlist;
    • reuse the existing watched-subfolders episode matching to help identify likely series/season directories rather than adding another filename heuristic (this code has just been merged into the main branch to allow for its code to be re-used without issue - see 7d0ee92);
    • keep folder searching enabled after slow scans/timeouts and make the user warning less aggressive and non-repeating.

    I've also updated the Markdown development/contribution guidance associated with #763 so it is more useful for this kind of AI-assisted work. Please use the updated guidance and the live repository when making the changes.

    I've attached a detailed implementation specification covering the proposed behaviour, responsibilities, fallback logic and test cases:

    PR-799-media-directory-monitoring-spec.md

    Network storage and file system monitoring isn't really my area so if you can think of a better implementation then give it a try and see how well it works, but this should hopefully be a useful starting point taking into account our goals and how Syncplay is intended to operate.

  6. AlySerry0 commented on Aug 19, 2026

    @AlySerry0
    Author

    Ran the §5 / test A direct-watch gate: a non-recursive WindowsApiObserver watch placed directly on the existing deep directory (P:\Animation\Anime\Naruto), with all changes made remotely from the Pi.

    Result — all four remote operations were delivered:

    created   ...\Naruto\.gate.mkv
    modified  ...\Naruto\.gate.mkv
    moved     ...\Naruto\.gate.mkv -> ...\Naruto\.gate2.mkv
    deleted   ...\Naruto\.gate2.mkv
    

    So on this Samba share the failure is specifically recursive/subtree delivery: a direct non-recursive watch on the directory itself reports remotely-originated changes reliably, while the recursive root watch does not surface them at depth.

    That matches the §5–§7 assumption — keep the recursive root watch as opportunistic coverage, and supplement it with a bounded set of direct non-recursive watches on the priority directories, which is exactly where remotely-added media would be caught. I'll work through the spec on that basis and run the watch-budget benchmarks (test B) as I go. Thanks for the very detailed spec and the updated agent docs — reading against those.

  7. AlySerry0 commented on Aug 19, 2026

    @AlySerry0
    Author

    Thanks again for the detailed spec and the updated agent docs — I've reworked #799 along those lines and force-updated the branch (rebased onto current master, so it builds on the merged watched-subfolders EpisodeFilenameParser). Full write-up is in the PR description; the short version:

    • New playlist-blind filemonitor.py owning the watchdog lifecycle and the thread→reactor handoff; recursive root watch + a budgeted set of supplemental non-recursive watches for Windows network roots.
    • Clear file events update mediaFilesCache directly and idempotently on the reactor thread; directory events use targeted subtree reconciliation with full reconciliation as the fallback. Single-flight worker; events during a scan collapse into one pending reconciliation; fileSwitchFoundFiles() coalesced.
    • Adaptive polling: slow safety reconciliation (300s) while current/next local files resolve, back to the existing ~30s cadence while a needed file is unresolved.
    • Degraded operational state (cleared after three clean full scans) separated from a single warning per configuration/session; folderSearchWarningThreshold redefined as grace on a 3s base, so no config migration. The folderSearchUseNotifications option is gone. Warning no longer mentions disabling.
    • getRelatedEpisodeDirectories reuses EpisodeFilenameParser (no os.walk, ranking hint only).

    On the §5 gate: confirmed a non-recursive WindowsApiObserver watch placed directly on the existing deep directory does receive the remote create/rename/delete events the recursive root watch misses — so the supplemental per-directory watch strategy is worth having, and it's in.

    Tested: event-application unit tests, a construct-under-reactor integration run, the FileMonitor live against my Samba share, a frozen build that bundles filemonitor + watchdog, and a real-session behaviour smoke (join/playlist/EOF/file-switch — no seek/pause/playlist/protocol change). Still to do with a hand: the Test B watch-budget benchmark (1/8/16/32/64 network watches) and the macOS/Linux parts of the matrix.

    Happy to adjust the constants, naming, or the watchdog/Python-version decision (I pinned watchdog<4 to keep the current declared minimum Python rather than bump it unilaterally).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions