Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration!How!?

Getting Started

GitHub GitHub stars for shinyorg/music
Downloads NuGet downloads for Shiny.Music
Frameworks
.NET
.NET MAUI
Operating Systems
Android
iOS

Shiny.Music provides a unified API for accessing the device music library on Android and iOS. It supports permission management, querying track metadata, playing music files, and copying tracks (where platform restrictions allow).

  • Request and check music library permissions on both platforms
  • Query all music tracks or search by title, artist, or album
  • Resolve tracks and playlists by identifier — GetTrackByIdAsync, GetTracksByIdsAsync, and GetPlaylistByIdAsync for rehydrating saved queues, favorites, or a resumed session
  • Browse by genre, year, or decade — each with track counts
  • Browse playlists and their tracks
  • Filter tracks by any combination of genre, year, decade, and search query via MusicFilter
  • Cross-dimensional browsing — genres by decade, years by genre, etc.
  • Play, pause, resume, stop, and seek within tracks
  • Duck the currently playing music for announcements or TTS via IMusicPlayer.Duck(DuckOptions?) — an IAsyncDisposable scope that restores full volume when disposed, with configurable duck Level and fade durations
  • Read and observe the device media volume via IMusicPlayer.Volume / VolumeChanged (setting is Android-only — guard with IsVolumeControlSupported)
  • Detect the audio output route with IAudioOutputDevices — speaker, wired/USB, Bluetooth, car, HDMI, or AirPlay — with IsWired() / IsBluetooth() / IsHeadphones() helpers and a Changed event for pausing when headphones are unplugged
  • Play Apple Music library tracks — including DRM-protected subscription content — via MPMusicPlayerController on iOS
  • Search the Apple Music streaming catalog with SearchCatalogAsync (iOS/Mac Catalyst) and stream results by CatalogId — no library membership required
  • Check for active streaming subscriptions
  • Fetch lyrics — plain text and synchronized LRC format via LRCLIB
  • Retrieve album artwork as cached file paths
  • Copy music files to app storage (non-DRM content only on iOS)
  • Event-driven playback state changes and completion notifications
  • Manage custom playlists — create, remove, and add tracks via IMediaLibrary
  • Track play counts per song via MusicMetadata.PlayCount
  • Expose the library and player to an LLM agent as Microsoft.Extensions.AI tools via the optional Shiny.Music.Extensions.AI package
Feature Android iOS
Permission Request
Query Tracks
Search Tracks
Browse by Genre
Browse by Year / Decade
Browse Playlists
Filter by Genre + Year/Decade
Local Playback ✅ (MPMusicPlayerController)
Catalog Search + Streaming ✅ (SearchCatalogAsync + CatalogId)
Ducking ✅ (exact level + fades) ✅ (AVAudioSession DuckOthers)
Volume — read / observe
Volume — set ❌ (NotSupportedException)
Output route + change events ✅ (all connected outputs; current is derived) ✅ (AVAudioSession.CurrentRoute; active route only)
Lyrics (Plain + Synced LRC) ✅ (LRCLIB) ✅ (LRCLIB + MPMediaItem.Lyrics)
Album Art ✅ (MediaStore) ✅ (MPMediaItem.Artwork, cached to file)
Copy Track ✅ (non-DRM only)
Streaming Subscription Check ❌ (always false) ✅ (MusicKit MusicSubscription)
Explicit Flag ✅ (via MPMediaItem.IsExplicitItem)
Custom Playlists ✅ (local JSON) ✅ (local JSON)
Play Count Tracking ✅ (local store) ✅ (MPMediaItem.PlayCount)
Changed files
  • MyApp/
Platforms/Android/AndroidManifest.xml
AndroidManifest.xml
1<?xml version="1.0" encoding="utf-8"?>2<manifest xmlns:android="http://schemas.android.com/apk/res/android">3	<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">4	</application>5	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>6	<uses-permission android:name="android.permission.BATTERY_STATS" />7	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />8	<uses-permission android:name="android.permission.INTERNET" />9	<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />10	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />11</manifest>

To let an LLM chat agent search, browse, play, and manage playlists through natural language, add the companion package:

Terminal window
dotnet add package Shiny.Music.Extensions.AI

See AI Tools for how to register and use it. It depends only on Microsoft.Extensions.AI.Abstractions and is AOT-compatible.

public class MyPage
{
readonly IMediaLibrary _library;
readonly IMusicPlayer _player;
readonly ILyricsProvider _lyrics;
public MyPage(IMediaLibrary library, IMusicPlayer player, ILyricsProvider lyrics)
{
_library = library;
_player = player;
_lyrics = lyrics;
}
async Task PlayFirstTrack()
{
// 1. Request permission
var status = await _library.RequestPermissionAsync();
if (status != PermissionStatus.Granted)
return;
// 2. Get all tracks
var tracks = await _library.GetAllTracksAsync();
if (tracks.Count == 0)
return;
// 3. Play the first track
await _player.PlayAsync(tracks[0]);
// 4. Duck the music while an announcement plays, then restore volume
await using (_player.Duck())
{
// play a TTS prompt or alert sound over the lowered music here
}
// 5. Get album artwork
var artPath = await _library.GetAlbumArtPathAsync(tracks[0].Id);
// 6. Fetch lyrics (plain or synced LRC)
var lyrics = await _lyrics.GetLyricsAsync(tracks[0]);
// 7. Listen for completion
_player.PlaybackCompleted += (s, e) =>
{
Console.WriteLine("Track finished!");
};
}
async Task BrowseAndFilter()
{
// Browse genres with track counts
var genres = await _library.GetGenresAsync();
foreach (var g in genres)
Console.WriteLine($"{g.Value} ({g.Count} tracks)");
// Browse decades
var decades = await _library.GetDecadesAsync();
foreach (var d in decades)
Console.WriteLine($"{d.Value}s ({d.Count} tracks)");
// Get rock tracks from the 1990s
var tracks = await _library.GetTracksAsync(new MusicFilter
{
Genre = "Rock",
Decade = 1990
});
// Get genres within the 2000s
var genresIn2000s = await _library.GetGenresAsync(new MusicFilter { Decade = 2000 });
// Browse playlists
var playlists = await _library.GetPlaylistsAsync();
foreach (var p in playlists)
Console.WriteLine($"{p.Name} ({p.SongCount} songs)");
// Get tracks in a playlist
var playlistTracks = await _library.GetPlaylistTracksAsync(playlists[0].Id);
}
}
claude plugin marketplace add shinyorg/skills
claude plugin install shiny-client@shiny
copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny-client@shiny
View shiny-client Plugin