Skip to content

Changelog

This changelog is used to track all major changes to Mopidy.

For older releases, see: Changelog 3.x · Changelog 2.x · Changelog 1.x · Changelog 0.x

v4.1.0 (UNRELEASED)

Mopidy 4.1 adds a media API, which reads media and playlist documents without playing them. The bundled file, stream and M3U extensions use it, and the new mopidy media commands show what it reads. The old scanner API is deprecated.

Media API

The new mopidy.media package reads media without playing it. Backends use it to get the metadata of a URI, to read playlist documents, and to find the URI to play for a radio station. It gives the metadata as a Track, so backends no longer have to convert GStreamer tags themselves.

Make one MediaReader with MediaReader.create() when your backend starts, and close it when your backend stops. The media API reference has an example.

  • MediaReader.read_media_info() reads the media info of one URI: the metadata as a Track, if the media decoded as audio, if the media allows seeking, and the embedded images. Tags that are not valid are left out. (#2325, !2332)

  • MediaReader.read_playlist_entries() fetches a playlist document from a file, http or https URI and parses its playlist entries. The reader keeps an HTTP connection pool that uses the Mopidy proxy config. A playlist document larger than 10 MiB raises MediaReaderError. (#2329, !2334, !2348)

  • MediaReader.find_playback_target() finds the URI to play from a URI that can be a playlist document, nested to any depth. It tries the alternatives of each playlist entry, and then the next entry. It stops the download at the first chunk that is not text, so it reads only the start of an audio stream. The PlaybackTarget has the URI, its media info and the playlist entry that it came from. (#2329, !2334)

  • parse_playlist_entries() parses the playlist entries of a playlist document from bytes, without I/O. It reads M3U, PLS, XSPF, ASX, ASX reference, and URI lists. The content decides the format. Each PlaylistEntry has the name and length from the playlist document, and all alternative URIs of the entry. HLS and DASH documents give no entries, as they usually should be passed directly to the playback engine. (#2327, !2333)

  • Upgrade the media reader pipeline from using the typefind and decodebin GStreamer elements to using parsebin.

    parsebin is the modern parsing stage also used internally by uridecodebin3. It demuxes and parses streams without ever plugging decoders, which is exactly what the media reader wants. This replaces the previous custom autoplug-select callback that faked the same behavior on top of decodebin. (#2127, !2338, !2352, !2353)

Bundled extensions

  • File extension: Read the metadata of files with the new MediaReader API. If some tags of a file are not valid, lookup() now keeps the other tags. Before, it gave no tags for the file. (#2325, !2332)

  • File extension: Play playlist documents, such as PLS and M3U files with radio streams, with MediaReader.find_playback_target(). lookup() gives the metadata of the playback target, with the URI of the playlist document. Before, playback of a playlist document failed with "This appears to be a text file". (#1529, !2351)

  • Stream extension: Find the URI to play with MediaReader.find_playback_target(). If the playlist of a radio station has more than one stream, the extension now tries each stream until one plays as audio. Before, it tried only the first stream. Playlists with the media type audio/x-mpegurl or audio/x-scpls are now read as playlists, and HLS streams are no longer read as playlists. (#2325, #2329, !2335)

  • Stream extension: The stream/timeout config value is now the total time to find the URI to play. Before, the limit for the total time was 1000 times too long. (#2329, !2335)

  • Stream extension: Play a stream as a live stream if it cannot seek and has no duration, such as most internet radio streams. GStreamer then does not pause playback to fill its buffer. Before, playback of some radio streams stopped for several seconds at random. (#1280, #1892, !2362)

  • M3U extension: Read playlists with parse_playlist_entries(). A file URI entry with no #EXTINF name now gets its name from the file name, as a path entry did before. In a playlist with no #EXTM3U header, a line is considered an entry only if it has a / or a file extension. (#2328, !2333)

Debug commands

  • Add the mopidy media command group, with the subcommands info, playlist-entries and playback-target. They show what the MediaReader reads from files and URIs, to debug missing metadata and radio stations that do not play. See Built in commands. (#2330, !2336)

  • Remove the python3 -m mopidy.audio.scan debug command. Use mopidy media info instead. The new command shows the metadata as a track, not the raw GStreamer tags. To see the raw tags, use gst-discoverer-1.0. (#2330, !2337)

Deprecations

  • Deprecate mopidy.audio.scan.Scanner, and the tag helpers repr_tags(), convert_taglist() and convert_tags_to_track() in mopidy.audio.tags. Use MediaReader.read_media_info() instead. The media info has the metadata as a Track, thus callers do not convert GStreamer tags. The deprecated API will be removed in Mopidy 5.0. (#2326, !2337)

  • Deprecate the Extension.version class attribute. Mopidy now gets the version from the distribution that registers the extension's entry point, and no longer reads the attribute. (!2298)

  • Deprecate mopidy.__version__. Mopidy no longer uses it itself. Use importlib.metadata.version("mopidy") instead. (!2298)

  • Deprecate PlaybackController.set_state(). Use play(), pause(), resume(), and stop() instead. The method now calls the playback command that gives the new state, so that the playback state and the audio pipeline do not differ. For example, set_state("stopped") stops the pipeline and releases the output device. Before, it changed only the playback state. Calls that do not match a playback command change nothing. The method will be removed in Mopidy 5.0. (!2355)

Core

  • In single and repeat mode with consume mode on, play the next track when a track ends. Before, core played the same track again, but consume mode had removed it from the tracklist. (!2356)

  • When the current track is removed from the tracklist, change to the track that next() gives, and skip the tracks that were removed in the same call. The playback state does not change: if playback was paused, it is paused on the new track, as with next(). If no track remains, playback stops. Before, core cleared the current track, but the removed track continued to play, and pause() and stop() could not stop it. (#1590, !2356)

  • In consume mode, continue with the track after an unplayable track. Before, core removed the unplayable track from the tracklist before it looked for the next track, so playback continued from the first track in the tracklist. In random mode, core also shuffled the full tracklist again. (!2357)

  • Keep playback paused when the track changes while paused, for example with next() or previous(). Core sends the events track_playback_ended for the old track, playback_state_changed from paused to paused, track_playback_started for the new track, and track_playback_paused for the new track. A seek while paused, after core queued the next track, goes back to the current track. Then core sends the same events with no track_playback_ended, and then seeked. Before, core set the playback state to playing when the new track started, and then back to paused, and sent the events for both state changes. (!2358)

  • Remove the workaround for #232. Core no longer changes the playback state to paused when a backend pauses the audio pipeline without a call to core. No known backend does this. Before, if a client called pause() and resume() with no wait between them, the playback state could stay paused while the audio played. (!2358)

Audio

  • Handle all GStreamer bus messages on the audio actor thread. Before, the GLib main loop thread and the actor thread changed the audio state at the same time, with no synchronization. The "Race condition happened" warning from #1222 and #1430 is removed, because this race is gone. (!2305)

  • Stop startup at once if the configured audio/output cannot be created. Before, Mopidy continued to build a pipeline with no output in it, while it shut down. (!2307)

  • Keep all bytes of the embedded images in the current tags of the audio actor. Before, a debug log message cut each bytes value in the tags to 10 bytes, also when debug logging was off. Thus get_current_tags() gave images that were not complete. (!2344)

  • The deprecated mopidy.audio.scan.Scanner now raises ScannerError for a URI scheme that no GStreamer element can open. Before, it raised GLib.Error. (!2347)

  • Fix a SystemError from an OverflowError in the audio actor on 32-bit platforms with PyGObject 3.50, the version in Debian 13. GStreamer's extended bus message types, such as the STREAM_COLLECTION messages from HLS streams, caused it. The audio actor now drops these messages. (#2030, !2359)

Other changes

  • HTTP extension: Fix the debug log message for sent JSON-RPC responses. It was never logged. (!2310)

  • Packaging: Remove the desktop file. It started Mopidy in a terminal window from the application menu, which is seldom useful for a server. The file was added for the Ubuntu Sound Menu, which no longer exists, and Mopidy-MPRIS stopped using it in 2018. (!2322)

Development process

  • Restructure the GStreamer code into smaller modules with typed interfaces, split it into a playback side and a media side, give the media reader the same structure as the playback pipeline, and delete unused code. (#2324, !2303, !2304, !2306, !2307, !2308, !2331, !2341, !2343, !2344, !2345, !2347)

  • Port the remaining unittest tests to pytest. (!2301)

  • Fail tests that hang for more than 60 seconds, and print the stack of all threads, instead of letting CI hang. (!2302)

  • Run the linters and type checkers on Python 3.15 in CI. (!2339)

v4.0.5 (2026-10-16)

  • File extension: Reject file:// URIs outside of file/media_dirs in lookup(), as browse() already does. Any client of the HTTP JSON-RPC API, which has no authentication, could find out if a file exists and if it is audio, read the tags of any audio file that the Mopidy process can read, and add the file to the tracklist. The raw file content was not exposed. (#2350, !2349)

v4.0.4 (2026-09-13)

  • Deps: Fix support for Cyclopts 3.12, the version in Debian stable. Cyclopts divides by the token count of the inner type when it converts a tuple, and a bool has no tokens, so mopidy --verbose stopped with a ZeroDivisionError. The systemd service passes --verbose, so Mopidy could not start at all on Debian 13. The parameter is a list now, which works with all supported Cyclopts versions. (!2300)

  • Docs: Install Mopidy-Spotify from the APT repository in the Debian guide, now that the extension is packaged there. (!2299)

  • Packaging: Add Keywords to the desktop file and Documentation to the systemd service, as Lintian asks for.

v4.0.3 (2026-09-06)

  • Config: Use platformdirs to find the XDG user directories that path config values expand to. Values set in $XDG_CONFIG_HOME/user-dirs.dirs are kept. Directories that are not set there, such as $XDG_MUSIC_DIR, now fall back to the standard location, like ~/Music, instead of staying unexpanded. (!2272)

  • Audio: Fix scanning of valid audio files. Gst.Structure has a __len__() method, so an empty structure is falsy. The truthiness checks in the scanner were replaced by explicit is not None tests. (#2278, !2280)

  • HTTP extension: Fix missing web client files in the wheel builds. The package-data config was not updated when the bundled extensions moved to mopidy._exts. (#2292, !2293)

  • HTTP extension: Fix core.describe() so that it lists all params of a method. Keyword-only params, like at_position and uris on core.tracklist.add(), were left out. Deprecated methods were described with the *args, **kwargs of the wrapper added by the deprecated decorator instead of their own params. (#2284, !2296)

  • HTTP extension: Set the Access-Control-Max-Age header on CORS preflight responses. Browsers now cache the preflight result for two hours, instead of sending a preflight request before every cross-origin request. (#2043, !2285)

  • Stream extension: Fix crash when a PLS playlist has a missing or invalid NumberOfEntries. The parser now ignores the declared count and uses the File keys that the playlist has, in numeric order. Entries that a too low count left out are now included. (!2276)

  • Stream extension: Skip entries with an empty value in PLS and ASX reference playlists. An empty first entry resolved to the playlist URI itself, which stopped the unwrapping and lost the valid entries after it. (!2276)

  • Docs: Update the macOS install instructions for current Homebrew. (!2291)

  • Dev: Add Python 3.15 to the test matrix, in tox and in CI. (!2297)

  • Dev: Fix the scanner test for plain text files on GStreamer >= 1.28.5. That version types .txt files as application/x-subtitle by extension, so the scan no longer fails. The test now asserts that plain text is not playable. (!2294)

v4.0.2 (2026-08-19)

  • Models: The musicbrainz_id fields on Album, Artist, and Track are again typed as str instead of UUID, like in Mopidy < 4.0, to allow scanning collections using alternative identifiers, or multiple identifiers in the same field. (!2283)

  • Models: The date field on Album and Track now also accepts the YYYY-MM format, in addition to YYYY and YYYY-MM-DD. GStreamer emits YYYY-MM for files that only have a year and a month in their date tag, and such files were previously rejected. (!2283)

  • Types: Deprecated mopidy.types.Date, mopidy.types.Year, and mopidy.types.DateOrYear. They are replaced by a single mopidy.types.ReleaseDate type, covering all three supported date formats. The old names remain importable, but they are now str subclasses instead of NewTypes, so they are not assignable to ReleaseDate and using them will fail type checking. At runtime they still behave as strings, and instantiating them emits a DeprecationWarning. They will be removed in a future release. (!2283)

  • Audio: convert_tags_to_track() now raises ScannerError if the tags can't be coerced into a valid Track. The file and stream backends catch this per URI and fall back to a track with just the URI, so that a single file with broken tags no longer fails the lookup of every other URI in the same batch. (!2283)

  • File extension: Strip whitespace from local/media_dirs config options. (#1965, !2275)

  • Stream extension: Support legacy ASX playlists. (#794, !2281)

v4.0.1 (2026-05-16)

  • Deps: Fix support for Cyclopts >= 3.12, < 4.3. We accidentally relied on features not present in the minimum supported version, 3.12. (!2257, !2261)

  • Deps: Require Tornado >= 6.4.2, as this is the exact version in Debian stable and we rely on testing improvements that landed in 6.4.1. (!2260)

  • Core API type hints: Change LibraryProvider methods to accept Iterable instead of list. (!2255)

  • Dev: Add tox env for running tests on lowest supported versions of deps (!2259)

v4.0.0 (2026-04-25)

Mopidy 4.0 is a major release bringing very few functional changes, but mostly focusing on modernizations to Mopidy's tech stack to keep it maintainable going into the future. In Mopidy 4.0 we've:

  • dropped support for old versions of our dependencies,
  • rebuilt data models using Pydantic, so data is validated and errors are caught at the edges of the application instead of on use,
  • removed a few long-deprecated APIs,
  • rebuilt the app startup sequence,
  • made many modules explicitly private,
  • added type hints to most of the source code, and
  • rebuilt our docs using Zensical.

Dependencies

We require newer versions of all of our dependencies. The versions we require are all available in Debian 13 Trixie, which is the latest Debian stable release at the time of writing.

  • Python >= 3.13

    • Support for 3.7-3.12 has been dropped. Python 3.13-3.14 has been added to the testing matrix.

    • setuptools is no longer a runtime dependency, as we've replaced the use of pkg_resources with importlib.metadata from Python's standard library.

  • GStreamer >= 1.26.2

    • PyGObject >= 3.50 is now an explicit Python dependency. Previously we assumed that you would install this together with your GStreamer installation.

    • Added a workaround for GStreamer's Gst.Structure().get_name() regression for versions v1.26.1 to v1.26.2 inclusive. (!2094)

    • We also added a workaround to not crash when using PyGObject < 3.55.3 together with GLib >= 2.88. (!2248)

  • Pykka >= 4.1

  • Tornado >= 6.4

  • Pydantic >= 2.10

    • This is a new dependency for Mopidy to replace our custom data models.
  • Cyclopts >= 3.12

    • This is a new dependency for Mopidy to build the command line interfaces, replacing our use of argparse.
  • Rich >= 3.9

    • This is a new dependency for Mopidy transitively via Cyclopts, which we've started to use directly as well to handle colorized log output.
  • HTTPX >= 0.28.1

    • HTTPX has replaced Requests as our HTTP client. (!2249)

CLI

  • Everything in the app startup from starting Mopidy to having a running server has been rebuilt. The code is now hopefully a lot easier to follow and maintain going forwards. (!2234)

  • Breaking change for extensions with their own CLI commands:

    The mopidy.commands module which extended on argparse to let extensions add their own CLI subcommands has been removed. We now use new dependency Cyclopts to build command line interfaces.

    The extensions maintained by the core team, including mopidy-local and mopidy-spotify, have been updated to use Cyclopts. The migration is pretty straight forward, but feel free to reach out for help with migrating your extension. (!2234)

  • Previously, different commands had different default logging levels. Now all commands, including mopidy itself, emit logs from warning level and higher by default. To get more detailed log output, add -v to the root command one or more times. The included mopidy.service systemd unit has been updated to include --verbose in its command, to continue logging on info level. (!2239)

  • Support for custom log colors via the logcolors config section has been removed. (!2241)

  • Log output to terminals are now colorized using Rich, which hopefully leads to a more pleasant and readable log output. Log colors can still be disabled by changing the logging/colors config. (!2241)

  • The command mopidy deps no longer repeats transitive dependencies that have already been listed. This reduces the length of the command's output drastically. (!2152)

Data models

Changes to the data models may affect any Mopidy extension or client.

  • The Track and Playlist models now require the uri field to always be set. (#2190, !2229)

  • The models are now based on Pydantic data classes, which means:

    • All model fields and the replace() method should work as before, so unless your extension modifies or adds models, this should not affect you.

    • Models are now type-checked at runtime. This should help catch bugs early, where the models are instantiated with data, instead of when the data is used. This means that if your extension is instantiating models with incorrect data, you should see errors sooner than before.

  • Since we now use Pydantic to convert data models to and from JSON, the old model machinery has been removed. This includes the following:

    • mopidy.models.ImmutableObject
    • mopidy.models.ValidatedImmutableObject
    • mopidy.models.ModelJSONEncoder
    • mopidy.models.model_json_decoder()
    • mopidy.models.fields.Collection
    • mopidy.models.fields.Date
    • mopidy.models.fields.Field
    • mopidy.models.fields.Identifier
    • mopidy.models.fields.Integer
    • mopidy.models.fields.String
    • mopidy.models.fields.URI

Core API

Changes to the Core API affect Mopidy frontends and clients.

Import paths

  • The core API is no longer exported from submodules, just from mopidy.core. Update your imports accordingly. (!2221)

    The removed modules are:

    • mopidy.core.actor
    • mopidy.core.history
    • mopidy.core.library
    • mopidy.core.listener
    • mopidy.core.mixer
    • mopidy.core.playback
    • mopidy.core.playlists
    • mopidy.core.tracklist
  • Moved mopidy.core.PlaybackState to mopidy.types.PlaybackState, where it has been unified with mopidy.audio.PlaybackState.

Root object

  • The Core class now requires the config argument to be present. As this argument is provided by Mopidy itself at runtime, this should only affect the setup of extension's test suites.

Playback controller

  • PlaybackController.play() no longer accepts TlTrack objects, which has been deprecated since Mopidy 3.0. Use tracklist IDs (tlid) instead. (#1855, !2150)

    There are a few APIs left that still support TlTrack objects where a simple tracklist ID should be used instead, but these have long been deprecated and will be removed in later releases, once our test suites have been updated to not rely so heavily on the use of TlTrack objects in API calls.

Backend API

Changes to the Backend API affect Mopidy backend extensions.

  • Document that Backend is instantiated with two keyword arguments, config and audio. (!2253)

  • Remove the attribute Backend.audio from the documentation. Mopidy core does not rely on a backend exposing this attribute, and the default backend constructor does not set this attribute. How the config and audio arguments are used is up to the backend implementation, but they are provided for backends to use if they need them. (!2253)

  • Added LibraryProvider.lookup_many() to take a list of URIs and return a mapping of URIs to tracks. If this method is not implemented then repeated calls to LibraryProvider.lookup() will be used as a fallback. (!2145)

  • Deprecated LibraryProvider.lookup(). Extensions should implement LibraryProvider.lookup_many() instead. (!2145)

Audio API

Changes to the Audio API only affect the few Mopidy backend extensions that interface with the audio layer themselves.

  • The audio API is no longer exported from submodules, just from mopidy.audio. Update your imports accordingly. The removed modules are:

    • mopidy.audio.actor
    • mopidy.audio.listener
    • mopidy.audio.utils
  • The old audio actor has been split into a Audio interface with the API used by core and backends, and a GstAudio implementation using GStreamer. The API is still very specific to GStreamer, but this split makes it a bit easier to mock out the audio layer in tests as it is clearer what is part of the required interface. (!2224)

  • Moved mopidy.audio.PlaybackState to mopidy.types.PlaybackState, where it has been unified with mopidy.core.PlaybackState.

  • The convert_tags_to_track() function now requires the track uri as an argument, so that it can construct valid Track objects.

  • Removed APIs only used by Mopidy-Spotify's bespoke audio delivery mechanism, which has not been used since Spotify shut down their libspotify APIs in May 2022. The removed functions/methods are:

    • mopidy.audio.Audio.emit_data()
    • mopidy.audio.Audio.set_appsrc()
    • mopidy.audio.Audio.set_metadata()
    • mopidy.audio.calculate_duration()
    • mopidy.audio.create_buffer()
    • mopidy.audio.millisecond_to_clocktime()

Type hints

  • Added type hints to most of the source code to make it safer to do changes, to catch bugs earlier, and to make it faster to navigate the code base. Pykka actor proxies are fully supported, so you should be able to see what type of object you get back when you call an actor method, and what type of arguments the method expects.

  • We now have a public mopidy.types module with types that are useful both for Mopidy core and extensions. This module will probably see tweaks going forward, and should not be considered entirely stable yet. However, if you limit your use of this module in your extension to with if TYPE_CHECKING: code blocks, your extension should continue to work at runtime even if this module sees breaking changes.

  • Switched from mypy to pyright and ty for type checking. The jury is still out on which we'll use long-term. (!2226)

Private modules

  • Moved bundled extensions to the private package mopidy._exts. (!2217)

    The removed modules are:

    • mopidy.file
    • mopidy.http
    • mopidy.m3u
    • mopidy.softwaremixer
    • mopidy.stream
  • Renamed modules to be explicitly private. (!2226)

    The removed modules are:

    • mopidy.config.keyring
    • mopidy.internal.*

Development process

  • We've dropped the split between the main and develop branches in our development process. We now use main for all development, and have removed the develop branch.

  • All docstrings have been ported from reStructuredText to Markdown in Google style. All docs have been ported from reStructuredText to Markdown, and we now use Zensical to build our docs. During the process, there has been quite a bit of improvements to the docs, but there are clearly corners that still need dusting and updating. (!2247)

  • The mopidy-ext-template extension template has been heavily modernized over the last year. All of the extensions maintained in the Mopidy GitHub organization have been updated to the latest version of the template, and we strongly recommend maintainers of other extensions to update their extensions to the latest version of the template as well. With the move from cookiecutter to Copier, it is now much easier to use the template to update existing extensions, not just create new ones.