v0.2.0
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.
DisklavierSharebrowses (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_directorymirrors 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_errormode for large catalogues. Verified against hardware with 1296 files.pysmbis 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 meansfrom aiodisklavier import DisklavierShareworks
after a plainpip install aiodisklavierwith no extras to remember.DisklavierShareErrorand itsNotFound/Exists/Authsubclasses, 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_CONTROLLERandSMB_PORTare exported for callers
building on the share.examples/sync_library.py, a runnable mirror-and-reindex script with a--flattenoption
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 anSMBBackendprotocol, so this choice is
replaceable if a later firmware moves on. async_sync_directoryexcludes macOS AppleDouble stubs (._*) and.DS_Storeby 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/.mp3with 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.wavand.mp3; narrowing a sync to{".mid"}produces
transcriptions that copy, index and play with the backing track silently missing, so
async_sync_directorywarns 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_directorylogs 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.pysets its notification up withDisklavierSharerather 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
FileNotFoundErroror
PermissionErrorit is, rather than as "Lost the SMB session" — and no longer costs a
healthy connection a pointless reconnect on the way out.OSErroris 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 aShareEntry.path,dir/../../../etc/passwdescapes the moment a
caller writesPath(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_treedeletes 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_errorsteps 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 newSyncAction.FAIL. - The
timeouta caller passes now reaches every SMB operation, not just the handshake.
pysmbgives each method its owntimeoutdefaulting 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_familyis passed explicitly when opening the session. It sits between the port and
the timeout inpysmb'sconnect, so passing three positional arguments landed the
timeout in it, andpysmbthen built a rawsocket.socket(<timeout>)rather than the
socket.create_connectionit should. On macOS a 30 s timeout meansAF_INET6, which
connects to an IPv4 literal anyway and hides the mistake; on Linux 30 is not that constant
and the connection fails outright. PassingNonerestores hostname resolution too.- A cancelled operation abandons its session instead of leaving it for the next caller.
asyncio.to_threadcannot 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'sNMBErrorderives
straight fromException, andnmbships its ownNotConnectedErrorthat is a different
class fromsmb.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 thatcontinue_on_errorshould have carried
— the collector never saw an exception it was not catching.