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 aTrack, 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 afile,httporhttpsURI 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 raisesMediaReaderError. (#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. ThePlaybackTargethas 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. EachPlaylistEntryhas 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
typefindanddecodebinGStreamer elements to usingparsebin.parsebinis the modern parsing stage also used internally byuridecodebin3. It demuxes and parses streams without ever plugging decoders, which is exactly what the media reader wants. This replaces the previous customautoplug-selectcallback that faked the same behavior on top ofdecodebin. (#2127, !2338, !2352, !2353)
Bundled extensions
-
File extension: Read the metadata of files with the new
MediaReaderAPI. 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 typeaudio/x-mpegurloraudio/x-scplsare now read as playlists, and HLS streams are no longer read as playlists. (#2325, #2329, !2335) -
Stream extension: The
stream/timeoutconfig 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(). AfileURI entry with no#EXTINFname now gets its name from the file name, as a path entry did before. In a playlist with no#EXTM3Uheader, a line is considered an entry only if it has a/or a file extension. (#2328, !2333)
Debug commands
-
Add the
mopidy mediacommand group, with the subcommandsinfo,playlist-entriesandplayback-target. They show what theMediaReaderreads 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.scandebug command. Usemopidy media infoinstead. The new command shows the metadata as a track, not the raw GStreamer tags. To see the raw tags, usegst-discoverer-1.0. (#2330, !2337)
Deprecations
-
Deprecate
mopidy.audio.scan.Scanner, and the tag helpersrepr_tags(),convert_taglist()andconvert_tags_to_track()inmopidy.audio.tags. UseMediaReader.read_media_info()instead. The media info has the metadata as aTrack, thus callers do not convert GStreamer tags. The deprecated API will be removed in Mopidy 5.0. (#2326, !2337) -
Deprecate the
Extension.versionclass 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. Useimportlib.metadata.version("mopidy")instead. (!2298) -
Deprecate
PlaybackController.set_state(). Useplay(),pause(),resume(), andstop()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 withnext(). If no track remains, playback stops. Before, core cleared the current track, but the removed track continued to play, andpause()andstop()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()orprevious(). Core sends the eventstrack_playback_endedfor the old track,playback_state_changedfrom paused to paused,track_playback_startedfor the new track, andtrack_playback_pausedfor 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 notrack_playback_ended, and thenseeked. 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()andresume()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/outputcannot 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.Scannernow raisesScannerErrorfor a URI scheme that no GStreamer element can open. Before, it raisedGLib.Error. (!2347) -
Fix a
SystemErrorfrom anOverflowErrorin the audio actor on 32-bit platforms with PyGObject 3.50, the version in Debian 13. GStreamer's extended bus message types, such as theSTREAM_COLLECTIONmessages 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
unittesttests 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 offile/media_dirsinlookup(), asbrowse()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
boolhas no tokens, somopidy --verbosestopped with aZeroDivisionError. 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
Keywordsto the desktop file andDocumentationto the systemd service, as Lintian asks for.
v4.0.3 (2026-09-06)
-
Config: Use
platformdirsto find the XDG user directories that path config values expand to. Values set in$XDG_CONFIG_HOME/user-dirs.dirsare 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.Structurehas a__len__()method, so an empty structure is falsy. The truthiness checks in the scanner were replaced by explicitis not Nonetests. (#2278, !2280) -
HTTP extension: Fix missing web client files in the wheel builds. The
package-dataconfig was not updated when the bundled extensions moved tomopidy._exts. (#2292, !2293) -
HTTP extension: Fix
core.describe()so that it lists all params of a method. Keyword-only params, likeat_positionandurisoncore.tracklist.add(), were left out. Deprecated methods were described with the*args, **kwargsof the wrapper added by thedeprecateddecorator instead of their own params. (#2284, !2296) -
HTTP extension: Set the
Access-Control-Max-Ageheader 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 theFilekeys 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
.txtfiles asapplication/x-subtitleby 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_idfields onAlbum,Artist, andTrackare again typed asstrinstead ofUUID, like in Mopidy < 4.0, to allow scanning collections using alternative identifiers, or multiple identifiers in the same field. (!2283) -
Models: The
datefield onAlbumandTracknow also accepts theYYYY-MMformat, in addition toYYYYandYYYY-MM-DD. GStreamer emitsYYYY-MMfor 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, andmopidy.types.DateOrYear. They are replaced by a singlemopidy.types.ReleaseDatetype, covering all three supported date formats. The old names remain importable, but they are nowstrsubclasses instead ofNewTypes, so they are not assignable toReleaseDateand using them will fail type checking. At runtime they still behave as strings, and instantiating them emits aDeprecationWarning. They will be removed in a future release. (!2283) -
Audio:
convert_tags_to_track()now raisesScannerErrorif the tags can't be coerced into a validTrack. Thefileandstreambackends 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_dirsconfig 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
LibraryProvidermethods to acceptIterableinstead oflist. (!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_resourceswithimportlib.metadatafrom 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.
- This is a new dependency for Mopidy to build the command line interfaces,
replacing our use of
-
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.commandsmodule which extended onargparseto 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-localandmopidy-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
mopidyitself, emit logs from warning level and higher by default. To get more detailed log output, add-vto the root command one or more times. The includedmopidy.servicesystemd unit has been updated to include--verbosein its command, to continue logging on info level. (!2239) -
Support for custom log colors via the
logcolorsconfig 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/colorsconfig. (!2241) -
The command
mopidy depsno 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
TrackandPlaylistmodels now require theurifield 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.ImmutableObjectmopidy.models.ValidatedImmutableObjectmopidy.models.ModelJSONEncodermopidy.models.model_json_decoder()mopidy.models.fields.Collectionmopidy.models.fields.Datemopidy.models.fields.Fieldmopidy.models.fields.Identifiermopidy.models.fields.Integermopidy.models.fields.Stringmopidy.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.actormopidy.core.historymopidy.core.librarymopidy.core.listenermopidy.core.mixermopidy.core.playbackmopidy.core.playlistsmopidy.core.tracklist
-
Moved
mopidy.core.PlaybackStatetomopidy.types.PlaybackState, where it has been unified withmopidy.audio.PlaybackState.
Root object
- The
Coreclass now requires theconfigargument 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 acceptsTlTrackobjects, which has been deprecated since Mopidy 3.0. Use tracklist IDs (tlid) instead. (#1855, !2150)There are a few APIs left that still support
TlTrackobjects 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 ofTlTrackobjects in API calls.
Backend API
Changes to the Backend API affect Mopidy backend extensions.
-
Document that
Backendis instantiated with two keyword arguments,configandaudio. (!2253) -
Remove the attribute
Backend.audiofrom the documentation. Mopidy core does not rely on a backend exposing this attribute, and the default backend constructor does not set this attribute. How theconfigandaudioarguments 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 toLibraryProvider.lookup()will be used as a fallback. (!2145) -
Deprecated
LibraryProvider.lookup(). Extensions should implementLibraryProvider.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.actormopidy.audio.listenermopidy.audio.utils
-
The old audio actor has been split into a
Audiointerface with the API used by core and backends, and aGstAudioimplementation 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.PlaybackStatetomopidy.types.PlaybackState, where it has been unified withmopidy.core.PlaybackState. -
The
convert_tags_to_track()function now requires the trackurias an argument, so that it can construct validTrackobjects. -
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.typesmodule 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 withif 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.filemopidy.httpmopidy.m3umopidy.softwaremixermopidy.stream
-
Renamed modules to be explicitly private. (!2226)
The removed modules are:
mopidy.config.keyringmopidy.internal.*
Development process
-
We've dropped the split between the
mainanddevelopbranches in our development process. We now usemainfor all development, and have removed thedevelopbranch. -
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-templateextension 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.