Skip to content

v0.2.0

Choose a tag to compare

@reubenbijl reubenbijl released this 16 Aug 01:15
· 2 commits to main since this release

A minor rather than a patch: the library gains a second transport and a new dependency.

Added

  • SMB access to the piano's PC Sharing Folder. DisklavierShare browses (async_list,
    async_walk, async_stat, async_exists, async_list_shares), transfers
    (async_upload, async_upload_bytes, async_download, async_download_bytes) and
    changes the share (async_makedirs, async_rename, async_delete,
    async_remove_directory, async_delete_tree) — the route for getting your own MIDI onto
    the instrument, which the HTTP API has no way to do.
  • async_sync_directory mirrors a local tree onto the share, sending only what is missing
    or has changed, with filtering, optional pruning, a dry run, per-file progress and a
    continue_on_error mode for large catalogues. Verified against hardware with 1296 files.
  • pysmb is now a dependency of the package rather than an optional extra. It is pure
    Python with one small dependency of its own, so requiring it costs a caller who only wants
    the HTTP API almost nothing, and it means from aiodisklavier import DisklavierShare works
    after a plain pip install aiodisklavier with no extras to remember.
  • DisklavierShareError and its NotFound / Exists / Auth subclasses, carrying the
    path and the server's NT status so callers can branch on cause rather than on a message.
  • INDEXED_DEPTH_LIMIT, PLAYABLE_SUFFIXES, AUDIO_SUFFIXES, DEFAULT_EXCLUDES,
    SHARE_PC_SHARING, SHARE_ENSPIRE_CONTROLLER and SMB_PORT are exported for callers
    building on the share.
  • examples/sync_library.py, a runnable mirror-and-reindex script with a --flatten option
    for libraries with one grouping level too many.
  • docs/enspire-api.md §8 documents the share: the SMB1-only negotiation, the guest access
    model, the NT status codes, the two-level indexing limit, and measured throughput.

Notes

  • The SMB dependency is pysmb, not smbprotocol. The firmware runs Samba 3.0.37, whose
    newest protocol dialect is SMB1/NT1 — Samba did not support SMB2 until 3.6 — so an SMB2
    negotiate gets the socket closed without a reply and smbprotocol, which supports SMB 2.0.2
    upwards, cannot talk to the piano at all. pysmb still speaks NT1, and negotiates SMB2 where
    a server offers it. The transport sits behind an SMBBackend protocol, so this choice is
    replaceable if a later firmware moves on.
  • async_sync_directory excludes macOS AppleDouble stubs (._*) and .DS_Store by default.
    The firmware indexes an AppleDouble stub as a song in its own right, and loading one
    silently resets the piano to the first built-in song.
  • An audio file beside a MIDI file is that song's backing track, not a song of its own.
    song.mid + song.wav/.mp3 with matching basenames is one SMF+Audio song: the keys
    play the MIDI, the speakers play the audio, and the audio's length becomes the reported
    duration. Confirmed by adding a WAV to an indexed MIDI — the song count did not change and
    the duration moved from the MIDI's 187.6 s to the audio's 190.6 s. PLAYABLE_SUFFIXES
    therefore includes .wav and .mp3; narrowing a sync to {".mid"} produces
    transcriptions that copy, index and play with the backing track silently missing, so
    async_sync_directory warns when it sends a MIDI whose companion was filtered out.
  • The piano indexes only two folder levels below the share root. A file any deeper is
    copied without complaint and never appears in the library — no error from the write, none
    from the reindex. Established by planting one file at three depths and reindexing.
    async_sync_directory logs a warning when it uploads past the limit; it does not
    restructure a tree, because where to fold the extra level is the caller's decision.

Changed

  • examples/doorbell.py sets its notification up with DisklavierShare rather than telling
    you to copy from the Finder, which would leave an AppleDouble stub on the share.

Fixed

  • A local file that cannot be read is reported as the FileNotFoundError or
    PermissionError it is, rather than as "Lost the SMB session" — and no longer costs a
    healthy connection a pointless reconnect on the way out. OSError is the net used to spot
    a dead socket, so a local read error raised inside the worker thread was landing in it.
  • Listing rows whose name is not a plain filename are dropped, with a warning. SMB cannot
    express a separator inside a name, so a row carrying one is a misbehaving or hostile
    server; passed on as a ShareEntry.path, dir/../../../etc/passwd escapes the moment a
    caller writes Path(local_dir) / entry.path. This library's own calls were already stopped
    by the path guard on the way back, but the paths it hands out have to be safe too.
  • async_delete_tree deletes a path that turns out to be a file, instead of walking it and
    reporting "no such path" for something plainly there.
  • The progress stream reaches its total when continue_on_error steps over a file. A skipped
    file emitted no callback, so a bar driven by it stuck short of 100% for the rest of the run.
    Failures now report as the new SyncAction.FAIL.
  • The timeout a caller passes now reaches every SMB operation, not just the handshake.
    pysmb gives each method its own timeout defaulting to 30 s, so leaving them unset made
    the constructor argument govern opening the session and nothing else — a caller asking for
    120 s to cover slow transfers quietly got 30.
  • sock_family is passed explicitly when opening the session. It sits between the port and
    the timeout in pysmb's connect, so passing three positional arguments landed the
    timeout in it, and pysmb then built a raw socket.socket(<timeout>) rather than the
    socket.create_connection it should. On macOS a 30 s timeout means AF_INET6, which
    connects to an IPv4 literal anyway and hides the mistake; on Linux 30 is not that constant
    and the connection fails outright. Passing None restores hostname resolution too.
  • A cancelled operation abandons its session instead of leaving it for the next caller.
    asyncio.to_thread cannot cancel the worker, so it is still mid-request on the socket when
    the lock is released; reusing that connection put two writers on one SMB stream, which
    desynchronises it and surfaces later as a framing error on some unrelated call.
  • NetBIOS-layer failures are treated as a lost session, so they get the same reconnect and
    retry as any other transport fault instead of escaping the library untranslated. pysmb
    has two exception trees and this is the easy one to miss: nmb's NMBError derives
    straight from Exception, and nmb ships its own NotConnectedError that is a different
    class from smb.base's, so neither is caught by anything aimed at the SMB layer. Found
    partway through an 865 MB transfer, where a framing error ("Invalid protocol header for
    Direct TCP session message") took down a run that continue_on_error should have carried
    — the collector never saw an exception it was not catching.