Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 79 additions & 79 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,119 +2,120 @@ name: Build

on:
push:
branches: [master]
tags: ['v*']
pull_request:
branches: [master]

permissions:
contents: read

jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
fetch-depth: 0

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y autoconf automake intltool python3

- name: Autogen
run: ./autogen.sh --sysconfdir=/etc

- name: Build
run: make

- name: Prepare release assets
run: |
mkdir -p release
cp src/mpDris2 release/
cp src/mpDris2.service release/
cp src/org.mpris.MediaPlayer2.mpd.service release/
cp src/mpDris2.conf release/
cp src/mpdris2.desktop release/

- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: mpDris2
path: release/
python-version: '3.11'
- name: Install runtime + dev deps
run: pip install -e .[dev,cover]
# Fail fast on a tag that doesn't match mpdris2/__init__.py,
# before the deb job (and the rest of the matrix) burns minutes.
- name: Check tag matches __init__.py
if: startsWith(github.ref, 'refs/tags/v')
run: make check-tag
- name: ruff
if: always()
run: make lint-ruff
- name: mypy
if: always()
run: make lint-mypy
- name: Pytest
if: always()
run: make test

deb:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
# Build inside the same Debian release we target (trixie) so dh-python,
# debhelper and python3-* versions match what users actually have.
container:
image: debian:trixie
steps:
- uses: actions/checkout@v5
- name: Install build deps
run: |
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates git make gettext \
build-essential debhelper dh-python pybuild-plugin-pyproject \
python3 python3-setuptools python3-babel devscripts

- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Install Debian build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential debhelper dh-python \
autoconf automake gettext intltool \
python3 devscripts

- name: Sync changelog with tag version
- name: Sync changelog from __init__.py
if: startsWith(github.ref, 'refs/tags/v')
env:
DEBFULLNAME: GitHub Actions
DEBEMAIL: actions@github.com
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
CL_VERSION="$(dpkg-parsechangelog -S Version)"
if [ "${TAG_VERSION}" != "${CL_VERSION}" ]; then
echo "Tag ${TAG_VERSION} differs from changelog ${CL_VERSION} — prepending entry"
dch --newversion "${TAG_VERSION}" --distribution unstable --urgency medium \
"Release ${TAG_VERSION}"
else
echo "Tag matches changelog (${CL_VERSION})"
fi
run: make sync-deb

- name: Build .deb
run: dpkg-buildpackage -b -us -uc
run: make deb

- name: Collect .deb
run: |
mkdir -p deb
mv ../*.deb deb/
ls -lh deb/
mkdir -p dist
mv ../mpdris2_*.deb dist/
ls -lh dist/

- name: Upload .deb artifact
uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v6
with:
name: mpdris2-deb
path: deb/*.deb
path: dist/*.deb
if-no-files-found: error

sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gettext
pip install build
- name: Compile .mo catalogs
run: make i18n-compile
- name: Build sdist
run: python -m build --sdist
- uses: actions/upload-artifact@v6
with:
name: mpdris2-sdist
path: dist/*.tar.gz
if-no-files-found: error

release:
needs: [build, deb]
needs: [lint, deb, sdist]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download tarball assets
uses: actions/download-artifact@v6
with:
name: mpDris2
path: release

- name: Download .deb
uses: actions/download-artifact@v6
- uses: actions/download-artifact@v8
with:
name: mpdris2-deb
path: deb

- name: Create release archive
run: tar czf mpDris2-${GITHUB_REF_NAME#v}.tar.gz -C release .

- name: Create release
uses: softprops/action-gh-release@v2
path: dist/
- uses: actions/download-artifact@v8
with:
name: mpdris2-sdist
path: dist/
- uses: softprops/action-gh-release@v3
with:
files: |
mpDris2-*.tar.gz
deb/*.deb
dist/*.deb
dist/*.tar.gz
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }}

Expand All @@ -123,8 +124,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Trigger apt-repo rebuild
uses: peter-evans/repository-dispatch@v3
- uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.APT_REPO_TOKEN }}
repository: b0bbywan/odio-apt-repo
Expand Down
38 changes: 16 additions & 22 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
Makefile
Makefile.in
Makefile.in.in
compile
config.*
configure
depcomp
install-sh
missing
aclocal.m4
autom4te.cache
org.mpris.MediaPlayer2.mpd.service
POTFILES
*.gmo
*.pot
stamp-it
mpDris2
mpDris2.py
mpDris2.service
# Python build artifacts
__pycache__/
*.pyc
*.pyo
build/
dist/
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
mpdris2/locale/

# debhelper / dpkg-buildpackage artifacts
debian/.debhelper/
debian/autoreconf.after
debian/autoreconf.before
debian/debhelper-build-stamp
debian/files
debian/mpDris2.1
debian/mpdris2/
debian/mpdris2.debhelper.log
debian/mpdris2.postinst.debhelper
Expand All @@ -34,3 +23,8 @@ debian/tmp/
*.deb
*.buildinfo
*.changes

# Python virtualenv (local dev)
.venv/
__pycache__/
*.pyc
91 changes: 91 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

mpDris2 is a Python 3 asyncio daemon that provides MPRIS 2 (Media Player Remote Interfacing Specification) D-Bus interface support for MPD (Music Player Daemon). It monitors a local or remote MPD server and exposes it as an MPRIS2-compliant media player on the session D-Bus, using `python-mpd2` for the MPD protocol and `dbus-fast` for the MPRIS interface. No threads, no GLib.

## Build System

`pyproject.toml` (setuptools backend) is the build entry point; `mpdris2/__init__.py` is the version source of truth.

```bash
# Dev install + tooling
pip install -e '.[dev,cover]'

# Lint, type-check, tests
make lint
make test

# Build the Debian package (needs a Debian toolchain — use the
# debian:trixie container that CI uses for parity).
make deb

# Sync the .deb changelog with __init__.py before tagging a release
make sync-deb
```

For Nix users, `shell.nix` provides a development shell.

## Source Structure

Flat package at `mpdris2/`:

| Module | Responsibility |
|--------|----------------|
| `__init__.py` | `__version__` (parsed by `scripts/version.py` and `pyproject.toml`) |
| `__main__.py` | `python -m mpdris2` entry point |
| `cli.py` | argparse, INI config loading, gettext bind, `asyncio.run(run(cfg, args))` |
| `bridge.py` | `MpdMprisBridge` — MPD connect/reconnect, D-Bus export, MPRIS callbacks, idle-driven `refresh()` |
| `mpd_client.py` | `mpd.asyncio.MPDClient` wrapper: connect-with-backoff + capability probe |
| `mpris.py` | `dbus_fast.ServiceInterface` classes: `MediaPlayer2` (root) + `MediaPlayer2Player` |
| `translate.py` | Pure MPD song dict → MPRIS Metadata dict (`Variant`-wrapped) |
| `cover.py` | 5-step async cover pipeline (MPD readpicture → filesystem regex → MPD albumart → CUE/cdda fallback → XDG cache) |
| `notify.py` | Desktop notifications via `org.freedesktop.Notifications` over dbus-fast |
| `locale/` | Compiled `.mo` files (built from `po/*.po`, shipped as package data) |

Helper scripts: `scripts/version.py` parses `__init__.py` and produces both PEP 440 and Debian-sortable forms. Used by `make sync-deb` and `make check-tag`.

## Runtime Dependencies

- Python 3.11+
- `python-mpd2 >= 3.1`
- `dbus-fast >= 2.0`

Dev: `pytest`, `pytest-asyncio`, `mypy`, `ruff`, `babel`, `build`.

## Configuration

User config at `~/.config/mpDris2/mpDris2.conf` (INI), falling back to `/etc/mpDris2/mpDris2.conf`. Example shipped at `/usr/share/doc/mpdris2/mpdris2.conf`.

Sections in current use:
- `[Connection]` — `host`, `port`, `password`
- `[Library]` — `music_dir`, `cover_regex`
- `[Notify]` — `notify` (bool)

CLI overrides config: `-H`/`--host`, `-p`/`--port`, `--music-dir`, `--config`, `--use-journal`, `--no-reconnect`, `-v`/`--verbose`.

## i18n

`po/fr.po` + `po/nl.po`. `babel.cfg` controls extraction, `msgfmt` (from `gettext`) compiles `.mo` files.

```bash
make i18n-extract # refresh po/mpdris2.pot from current source
make i18n-compile # rebuild mpdris2/locale/*/LC_MESSAGES/mpdris2.mo
```

The runtime catalog lookup is wired in `cli.py` (`gettext.bindtextdomain` + `gettext.textdomain` against `mpdris2/locale/`). Modules use `from gettext import gettext as _`.

## Packaging

- **Debian**: `debian/` uses `pybuild-plugin-pyproject` (no autotools). `debian/rules` calls `msgfmt` before `dh_auto_build` to compile `.mo` files; data files (`data/*.{service,desktop,conf}`) are listed in `debian/mpdris2.install`.
- **Systemd**: user unit `data/user/mpDris2.service` (`Type=dbus`, `BusName=org.mpris.MediaPlayer2.mpd`, `Restart=on-failure`, `ConditionUser=!root` / `ConditionUser=!@system`). Not auto-enabled on install — D-Bus activation kicks it in on the first MPRIS call.
- **D-Bus activation**: `data/dbus-1/org.mpris.MediaPlayer2.mpd.service` → `/usr/share/dbus-1/services/`.
- **CI**: `.github/workflows/build.yml` runs lint + tests on every PR, builds the `.deb` in a `debian:trixie` container on tags, creates a GitHub release, and dispatches to the private `b0bbywan/odio-apt-repo` for APT-repo rebuild.

## Notes

- Media keys: the original GNOME `org.gnome.SettingsDaemon.MediaKeys` grab was dropped during the asyncio rewrite. Modern desktops (GNOME ≥ 3.6, KDE, Sway/Hyprland with `playerctld`) route media keys through MPRIS2 directly.
- The `[Bling] mmkeys` config key is no longer honoured (the GNOME MediaKeys grab was dropped in the asyncio rewrite). `[Bling] cdprev` and `[Bling] notify_paused` are still honoured by `bridge.py`.
- The legacy `src/mpDris2.in.py` + autotools build was removed in the same refactor; see `docs/refactor-asyncio-dbus-fast.md` for the migration plan.
Loading