Skip to content

Bootstrap pyproject + asyncio package skeleton#3

Merged
b0bbywan merged 9 commits into
masterfrom
refactor/asyncio
May 19, 2026
Merged

Bootstrap pyproject + asyncio package skeleton#3
b0bbywan merged 9 commits into
masterfrom
refactor/asyncio

Conversation

@b0bbywan
Copy link
Copy Markdown
Owner

First step of the autotools → pyproject + asyncio + dbus-fast refactor (see docs/refactor-asyncio-dbus-fast.md). Adds the mpdris2/ package with init, main, cli and a daemon stub that just awaits SIGTERM, the pyproject.toml + scripts/version.py + slim dev Makefile + babel.cfg, and tests/test_cli.py covering argparse and config loading. The old autotools build stays alongside until PR 4 cuts it over.

@b0bbywan b0bbywan force-pushed the refactor/asyncio branch 13 times, most recently from ed7ffb3 to cbe5e11 Compare May 18, 2026 22:52
b0bbywan and others added 4 commits May 19, 2026 18:34
First step of the autotools → pyproject + asyncio + dbus-fast refactor
(see docs/refactor-asyncio-dbus-fast.md). Adds the mpdris2/ package with
__init__, __main__, cli and a daemon stub that just awaits SIGTERM, the
pyproject.toml + scripts/version.py + slim dev Makefile + babel.cfg, and
tests/test_cli.py covering argparse and config loading. The old autotools
build stays alongside until PR 4 cuts it over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the dbus-python / GLib glue with dbus-fast ServiceInterface
classes (MediaPlayer2 + MediaPlayer2Player) and an asyncio
mpd.asyncio wrapper with exponential-backoff connect. The daemon's
run() coroutine acquires the bus, watches MPD idle events on
player/mixer/options/playlist, and translates state changes into
PropertiesChanged emissions.

Methods (Play/Pause/PlayPause/Stop/Next/Previous/Seek/SetPosition) and
R/W properties (Volume / LoopStatus / Shuffle) round-trip to MPD;
Metadata stays empty until PR 3 wires translate.py + cover.py.

The mmkeys GNOME-SettingsDaemon grab is intentionally dropped — modern
desktops route media keys through MPRIS2 directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure mapping of MPD currentsong fields onto MPRIS Metadata keys with
dbus-fast Variants. DEFAULT_URL_HANDLERS lists the schemes that should
pass through unchanged (http, https, …) so streamed tracks keep their
scheme intact instead of being prefixed with the music library path.

CDDA / CUE tracks frequently carry only ``albumartist``. MPRIS clients
overwhelmingly read ``xesam:artist`` for the track-row artist column,
so the translator mirrors albumArtist into artist when artist is
missing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5-step pipeline tried in order, authoritative-first:

1. MPD ``readpicture`` — embedded art in the audio file (FLAC PICTURE,
   ID3 APIC, …). Server-side parsing, works for local and remote MPD.
2. Filesystem regex match in the song's directory — uses the
   configurable ``cover_regex`` (default catches cover.*, folder.*,
   album.*, front.*) so non-standard names like ``folder.jpg`` are
   resolved without any temp-file copy.
3. MPD ``albumart`` — MPD resolves cover.{png,jpg,jxl,webp} server-side
   from the song's directory. Useful for remote MPD or standard-named
   covers that step 2 missed.
4. CUE/cdda fallback — when ``song_file`` is virtual (cdda://, http://,
   …) the audio file itself has no on-disk cover; look next to the
   loaded .cue playlist instead (FS scan first, MPD ``albumart`` as a
   remote fallback).
5. XDG cover cache — ``$XDG_CACHE_HOME/mpDris2/{artist}-{album}.jpg``
   for the optional MusicBrainz / Cover Art Archive fallback (PR 5).

Capability flags from the MPD command list drive whether readpicture
or albumart are attempted at all (older MPDs ship neither). Bytes
returned by MPD land in /tmp/cover-*.{jpg,png,…} and the URI of that
file is exposed as ``mpris:artUrl``; the FS regex step returns the
matched file's URI directly (no copy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@b0bbywan b0bbywan force-pushed the refactor/asyncio branch 2 times, most recently from 539dbce to 717a5e7 Compare May 19, 2026 17:43
b0bbywan and others added 5 commits May 19, 2026 21:27
Thin libnotify wrapper built on dbus-fast — no PyGObject dependency.
Notifier.notify(title, body, icon) fires (or replaces) a desktop
bubble through the freedesktop Notifications interface; the daemon
calls it on track change when the [Notify] section enables it.

Tuning lives in two frozen dataclasses:

* ``NotifierConfig(urgency, timeout)`` — display knobs forwarded to
  the notification server (urgency 0/1/2, expire_timeout in ms or -1
  to defer to the server's default).
* ``NotifyTemplates(summary, body, paused_summary, paused_body)`` —
  optional ``%placeholder%`` format strings consumed by
  ``format_template`` (placeholders: %album% %title% %id% %time%
  %timeposition% %date% %track% %disc% %artist% %albumartist%
  %composer% %genre% %file%). Empty templates fall back to built-in
  defaults; paused_* falls back to the playing template before the
  built-in default.

``_format_duration`` mirrors the original ``convert_timestamp`` so
%time%/%timeposition% render as ``M:SS`` (or ``H:MM:SS`` past an
hour). Unknown placeholders are left untouched rather than raising —
friendlier when users typo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refresh() now translates currentsong into MPRIS Metadata, resolves the
artUrl asynchronously via CoverFinder, toggles CanSeek on mpris:length
presence, and fires a libnotify bubble on track change. URL handlers
are probed from MPD's urlhandlers command at startup so streams keep
their scheme intact.

The music library path is resolved from CLI / config / XDG and
normalised to a file:// URL. [Notify] is preferred over the deprecated
[Bling] section for the enable flag.

Ported config keys from the historical mpDris2:

* ``[Bling] cdprev`` — CD-like Previous: when elapsed >= 3 s, restart
  the current track via ``seekid`` instead of skipping to the
  previous one.
* ``[Bling] notify_paused`` — also fire the bubble on track change
  while the player is paused (default off, matching the original).
* ``[Notify] urgency`` / ``timeout`` — wired into ``NotifierConfig``
  and forwarded to the notification server.
* ``[Notify] summary`` / ``body`` / ``paused_summary`` /
  ``paused_body`` — ``%placeholder%`` templates wired into
  ``_build_track_notification`` (paused state falls back to the
  playing template, then the built-in default; the paused default
  appends "(Paused)" to the body and swaps the icon for
  ``media-playback-pause-symbolic``). Read with ``raw=True`` so
  configparser doesn't try to interpolate the literal ``%`` tokens.

Closes feature parity with the original daemon minus mmkeys, which is
intentionally dropped — modern desktops consume MPRIS directly for
multimedia-key handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves packaging to pybuild-plugin-pyproject so debian/rules no longer
needs autoreconf; data files (systemd units, D-Bus activation, .desktop,
example config, manpage) live under data/ and are installed via
debian/mpdris2.install. CI is rebuilt on the snapclientmpris layout:
lint (pip install + ruff + mypy + pytest + check-tag) then deb (in a
debian:trixie container) then release + odio-apt-repo dispatch.

i18n migrates from intltool to babel + msgfmt: po/fr.po and po/nl.po
are preserved and re-merged against the new pot; mpdris2/locale/ is
built as package_data so gettext finds the catalogs at runtime.
cli.py binds the text domain; daemon.py uses ``from gettext import
gettext as _`` for the notification strings.

src/mpDris2.in.py and all autotools artefacts (configure.ac, autogen.sh,
Makefile.am, INSTALL, src/, po/POTFILES.in, po/LINGUAS, debian/*.in)
are gone. CLAUDE.md is rewritten to document the new layout. shell.nix
swaps dbus-python + pygobject3 for dbus-fast and adds babel/ruff/mypy/
pytest for dev. .gitignore is trimmed to the patterns that still apply.

Functional parity with the original daemon is preserved minus mmkeys
(intentionally dropped — modern desktops route media keys through
MPRIS2 directly). The systemd units keep ConditionUser=!root |
!@System and Restart=always RestartSec=5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Install: replace autogen/make with the apt.odio.love repository
  (primary) and a pipx/git fallback; drop the obsolete /usr/local
  prefix reference and the mutagen requirement (the asyncio package
  depends only on python-mpd2 and dbus-fast).
- Configuration: document every ported key — [Library] cover_regex /
  cover_cache_dir, [Bling] notify / notify_paused / cdprev, [Notify]
  urgency / timeout / summary / body / paused_summary / paused_body —
  with the full %placeholder% set in a comment.
- Note that [Bling] mmkeys is intentionally not ported: modern
  desktops (GNOME, KDE, sway) consume MPRIS directly for
  multimedia-key handling.
- Add a short Architecture section pointing at
  docs/refactor-asyncio-dbus-fast.md.
- Keep the Cover art resolution pipeline table introduced earlier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@b0bbywan b0bbywan force-pushed the refactor/asyncio branch from 717a5e7 to 791de7e Compare May 19, 2026 19:27
@b0bbywan b0bbywan merged commit 0e1aa32 into master May 19, 2026
5 checks passed
@b0bbywan b0bbywan deleted the refactor/asyncio branch May 20, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant