Media Providers
August 17, 2026 · View on GitHub
A MediaProvider is a catalog/search plugin: given a parsed media request it
returns candidate playables. Providers are the layer that turns "play Viagra
Boys" into a ranked list of streamable tracks, and they are the catalog layer
of the ovos-media stack.
Providers are the in-process replacement for the older OCP search skills
(OVOSCommonPlaybackSkill + @ocp_search). Instead of broadcasting
ovos.common_play.query over the bus and waiting for skills to answer, the OCP
pipeline loads providers in-process and calls each provider's search()
method directly. See Migration: from OCP search skills
below.
Plugin group
Providers register under the opm.media.provider entry-point group and subclass
ovos_plugin_manager.templates.media_provider.MediaProvider:
[project.entry-points."opm.media.provider"]
bandcamp = "ovos_media_provider_bandcamp:BandcampMediaProvider"
The entry-point key (bandcamp) is the provider's registry name, its skill_id
downstream, and its per-instance config key under media_providers.
The contract: one method
MediaProvider has a single abstract method:
def search(self, signals: Signals, lang: str = "en-us", *,
supported_playback_types: set[str] | None = None,
blocked_genres: set[str] | None = None,
region: str | None = None,
session_id: str | None = None) -> list[Release]:
...
There is nothing else to implement. There is no is_available, no matches, no
serves, no routing class attributes, and no QueryContext object, a provider
decides for itself whether it can serve a query and returns an empty list when it
cannot. Availability and routing are the provider's own concern.
class MediaProvider(metaclass=ABCMeta):
name: ClassVar[str] = "" # stable registry key / downstream skill_id
def __init__(self, config: Optional[dict] = None):
self.config = config or {}
@abstractmethod
def search(self, signals, lang="en-us", *, supported_playback_types=None, blocked_genres=None, region=None, session_id=None) -> list[Release]: ...
def shutdown(self) -> None: # optional, release resources
...
Arguments
-
signalsis amediavocab.Signals, the parsed request. The fields a provider usually reads aresignals.title(the search phrase) andsignals.artist(artist, or director for video).signals.mediumcarries the classifiedMediaTypeandsignals.content_genresthe requested genres. -
langis the BCP-47 language tag for the request (default"en-us"). -
the remaining keyword-only arguments carry what the pipeline knows about the request environment (explicitly named, not
**kwargs). A provider reads the ones it cares about and ignores the rest:Context kwarg Meaning supported_playback_typeswhich player surfaces the requesting device can render (so a video-only provider can bail out on an audio-only device) blocked_genresgenres the session/profile has blocked regionthe requester's region/country, for geo-scoped catalogs session_idthe originating MessageBus session
Return value
Return zero or more mediavocab.Release objects. Each Release is a typed,
playable catalog entry shared across the whole media ecosystem. A provider
returns playables, not identities, drop artist/label hits. Return an empty
list [] whenever the provider cannot serve the query: wrong media type, the
device can't render the result, a blocked genre, no network or API key, no
match, and so on.
Ranking rides on each result via Release.match_confidence (0.0-1.0).
The pipeline filters and ranks across all providers, so a provider only needs
to score within its own results.
mediavocab.Release in brief
A Release is a manifestation of a Work. The fields most relevant to playback:
| Field | Where | Description |
|---|---|---|
work.title | Release.work | Track / album / show title |
work.media_type | Release.work | MediaType of the content |
work.content_genres | Release.work | Genre tags |
uri | Release | Stream URL or deferred stream identifier (see below) |
image | Release | Cover / thumbnail art URL |
match_confidence | Release | 0.0-1.0 relevance score |
Most provider authors never build a Release by hand: the client library a
provider wraps (py_bandcamp, tutubo, radiosoma, …) already emits typed
Release objects, so a provider is a thin routing/filtering shim over that
client's search API.
Deferred stream URIs
A Release.uri may be a real URL or a deferred stream identifier of the form
"{sei}//{uri}" (for example youtube//https://youtube.com/watch?v=…). These
are resolved at playback time by the opm.ocp.extractor plugins, provider code
does not resolve streams itself. See Architecture and
Backends.
How the pipeline discovers and dispatches providers
The OCP pipeline loads every installed opm.media.provider entry point,
instantiating each with its per-provider config (media_providers.<name>) and
skipping any whose config sets "enabled": false.
At query time the pipeline:
- Classifies the utterance into a
Signals(media type, title, artist, genres). - Builds the context kwargs (supported playback types, blocked genres, region, session) from the requesting session's state.
- Calls
search(signals, lang, *, ...)on each loaded provider concurrently (thread pool). A provider that cannot serve the request returns[]. A misbehaving provider that raises is caught and treated as[], so it cannot abort a multi-provider dispatch. - Collects, filters, and ranks the returned
Releaseobjects across providers, then hands the winner(s) to theovos-mediadaemon to play.
Because providers run in-process, there is no bus round-trip and no per-skill announce/response handshake.
A minimal provider
A complete provider over a client library that already emits Release objects:
from typing import ClassVar, List, Optional
from ovos_utils.log import LOG
from mediavocab import Release, Signals
from ovos_plugin_manager.templates.media_provider import MediaProvider
from py_bandcamp import BandCamp
class BandcampMediaProvider(MediaProvider):
"""Search Bandcamp's public catalog for playable music releases."""
name: ClassVar[str] = "bandcamp"
def __init__(self, config: Optional[dict] = None):
super().__init__(config)
self.max_pages = self.config.get("max_pages", 1)
def search(self, signals: Signals, lang: str = "en-us", *,
supported_playback_types: set[str] | None = None,
blocked_genres: set[str] | None = None,
region: str | None = None,
session_id: str | None = None) -> List[Release]:
title = (signals.title or "").strip()
artist = (signals.artist or "").strip()
query = " ".join(p for p in (artist, title) if p).strip()
if not query:
return [] # nothing to search for
results: List[Release] = []
try:
for item in BandCamp.search(query, albums=True, tracks=True,
artists=False, labels=False,
max_pages=self.max_pages):
if isinstance(item, Release):
results.append(item)
# Entity (artist/label identity) hits are not playable, skip
except Exception:
LOG.exception("Bandcamp search failed")
return [] # no network / API failure, serve nothing
return results
Register it in pyproject.toml:
[project.entry-points."opm.media.provider"]
bandcamp = "ovos_media_provider_bandcamp:BandcampMediaProvider"
Notice the provider never declares what it serves up front: if the query is for a
movie, Bandcamp's client simply returns nothing and the provider yields [].
That is the whole gating contract.
Existing providers
Each wraps a standalone scraper/client library that emits mediavocab.Release
objects, and each replaces a legacy OCP search skill.
| Provider | Entry point | Serves | Auth needed | Replaces |
|---|---|---|---|---|
ovos-media-provider-youtube | youtube | video / music videos / podcasts | none | ovos-skill-youtube |
ovos-media-provider-youtube-music | youtube_music | music | none | ovos-skill-youtube-music |
ovos-media-provider-bandcamp | bandcamp | music | none | ovos-skill-bandcamp |
ovos-media-provider-soundcloud | soundcloud | music / playlists | none | ovos-skill-soundcloud |
ovos-media-provider-tunein | tunein | radio | none | ovos-skill-tunein |
ovos-media-provider-somafm | somafm | radio | none | ovos-skill-somafm |
ovos-media-provider-pyradios | pyradios | radio | none | ovos-skill-pyradios |
ovos-media-provider-mass | music_assistant | music / radio / podcasts / audiobooks | a running Music Assistant server | ovos-skill-music-assistant |
ovos-media-provider-spotify | spotify | music | Spotify OAuth (ocp_spotify credentials) | ovos-skill-spotify |
ovos-media-provider-news | news | broadcast news feeds | none | ovos-skill-news |
ovos-media-provider-local | local | local file-system media library | none | ovos-skill-local-media |
ovos-media-provider-radio-spain | radio_spain | Spanish internet radio | none | skill-ovos-radio-spain |
ovos-media-provider-radio-tuga | radio_tuga | Portuguese internet radio | none | skill-ovos-radio-tuga |
Configuration
Per-provider settings live under the top-level media_providers key of
mycroft.conf, keyed by the provider's entry-point name. Any keys are passed
through to the provider's config dict. Setting enabled: false disables a provider
without uninstalling it.
{
"media_providers": {
"bandcamp": {
"max_pages": 2
},
"youtube": {
"max_results": 10
},
"soundcloud": {
"enabled": false
}
}
}
See Configuration reference for the daemon-side media block
(backends, MPRIS).
Migration: from OCP search skills
Providers replace the catalog/search half of the old OCP design. The control and
playback halves are unchanged, those live in the OCP pipeline and the
ovos-media daemon.
| Old (OCP search skill) | New (MediaProvider) |
|---|---|
Subclass OVOSCommonPlaybackSkill | Subclass MediaProvider |
@ocp_search method returning MediaEntry/Playlist | search(signals, lang, *, ...) returning list[Release] |
Registered as a skill, replies to ovos.common_play.query over the bus | Registered under opm.media.provider, called in-process |
Routing implied by the skill's vocab and per-result media_type | The provider decides per call and returns [] when it can't serve |
Match score 0-100 on each MediaEntry | match_confidence 0.0-1.0 on each Release |
| Provider-specific result dicts | Typed mediavocab.Release, shared across the ecosystem |
The legacy approach is documented in OCP Skills and still works during the transition, but new catalog integrations should be written as MediaProviders.
See also
- Architecture, where providers sit in the full flow
- Backends, the playback plugins that consume provider results
- OCP Skills, the legacy search-skill approach this supersedes
- Configuration, daemon-side configuration
- mediavocab, the
Release/Signalsdata model
← Sessions · Home · Backends →