Lowers CPU usage:
- Only call refresh() when real artwork is found
- Treating NoArtwork as a valid cached result (art is not None check).
- Adding in-flight dedupe (pending_tracks) so the same track doesn't spawn parallel fetches.
Resolved resident memory growth during long-running idle sessions.
Fixes: https://github.com/00dani/mpd-now-playable/issues/3
The CoreFoundation asyncio bridge was leaking native resources over time.
This change explicitly invalidates one-shot CFRunLoop timers after execution,
invalidates timers on cancellation, and invalidates CFFileDescriptor objects
when unregistering the final callback source.
The CLI previously always started full app initialization (config load, listener
setup, receiver construction, and debug output) even when invoked with `-h`,
`--help`, `-v`, or `--version`. That made basic introspection noisy and buried
the requested output in startup logs.
Add early argument checks in `main()` so help/version requests are handled
immediately and the process exits without starting the app. Introduce a small
`print_help()` helper for consistent usage output.
This improves terminal UX and makes debugging/invocation checks much clearer by
keeping `-h` and `-v` output focused and predictable.
When enabled, this new receiver will spin up a local WebSockets server
and will send the currently playing song information to any clients that
connect. It's designed with Übersicht in mind, since WebSockets is the
easiest way to efficiently push events into an Übersicht widget, but
I'm sure it'd work for a variety of other purposes too.
Currently the socket is only used in one direction, pushing the current
song info from server to client, but I'll probably extend it to support
sending MPD commands from WebSockets clients as well.
The idea here is that there are other places that might want to know
what's playing, besides MPNowPlayingInfoCenter. For example, to expose
the now playing info to Übersicht efficiently, it needs to be available
from a web browser, ideally using WebSockets. So there could be a
receiver that runs a small WebSockets server and sends out now playing
info to anyone who connects.
Additionally, I hope to write receivers for MPRIS and for the System
Media Transport Controls on Windows, making mpd-now-playable equally
useful across all platforms.
None of this is implemented yet, of course, but I hope to get the
WebSockets receiver done pretty soon!
I'm going to keep the default behaviour unchanged. Unless you
explicitly configure different receivers in config.toml,
mpd-now-playable will just behave as an MPNowPlayingInfoCenter
integration as it's always done.
Everything now uses bog-standard Python dataclasses, with Pydantic
providing validation and type conversion through separate classes using
its type adapter feature. It's also possible to define your classes
using Pydantic's own model type directly, making the type adapter
unnecessary, but I didn't want to do things that way because no actual
validation is needed when constructing a Song instance for example.
Having Pydantic do its thing only on-demand was preferable.
I tried a number of validation libraries before settling on Pydantic for
this. It's not the fastest option out there (msgspec is I think), but it
makes adding support for third-party types like yarl.URL really easy, it
generates a nice clean JSON Schema which is easy enough to adjust to my
requirements through its GenerateJsonSchema hooks, and raw speed isn't
all that important anyway since this is a single-user desktop program
that reads its configuration file once on startup.
Also, MessagePack is now mandatory if you're caching to an external
service. It just didn't make a whole lot sense to explicitly install
mpd-now-playable's Redis or Memcached support and then use pickling with
them.
With all this fussing around done, I'm probably finally ready to
actually use that configuration file to configure new features! Yay!
python-mpd2 unreliably returns either a single value or a list of
values for commands like currentsong, which is super fun if you're
trying to write type stubs for it that statically describe its
behaviour. Whee.
Anyway, I ended up changing my internal song model to always use lists
for tags like artist and genre which are likely to have multiple values.
There's some finagling involved in massaging python-mpd2's output into
lists every time. However it's much nicer to work with an object that
always has a list of artists, even if it's a list of one or zero
artists, rather than an object that can have a single artist, a list of
multiple artists, or a null. So it's worth it.
The MPNowPlayingInfoCenter in MacOS only works with single string values
for these tags, not lists, so we have to join the artists and such into
a single string for its consumption. I'm using commas for the separator
at the moment, but I may make this a config option later on if there's
interest.