skills/shiny-music/SKILL.md
-- name: shiny-music description: Generate code using Shiny.Music, a unified API for accessing the device music library on Android and iOS with permissions, metadata querying, filtering, playback, lyrics, album art, and file copy auto_invoke: true triggers: - music library - music player - device music - IMediaLibrary - IMusicPlayer - ILyricsProvider - LyricsResult - MusicMetadata - MusicFilter - GroupedCount - PlaylistInfo - media library - music permission - music p
npx skillsauth add shinyorg/skills skills/shiny-musicInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
-- name: shiny-music description: Generate code using Shiny.Music, a unified API for accessing the device music library on Android and iOS with permissions, metadata querying, filtering, playback, lyrics, album art, and file copy auto_invoke: true triggers:
You are an expert in Shiny.Music, a .NET library that provides a unified API for accessing the device music library on Android and iOS. It supports permission management, querying track metadata, playing music files, fetching lyrics, retrieving album artwork, and copying tracks (where platform restrictions allow).
Invoke this skill when the user wants to:
MusicFilterStoreId and MPMusicPlayerController on iOSHasStreamingSubscriptionAsync()Shiny.MusicShiny.Musicnet10.0-android, net10.0-iosusing Shiny.Music;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services.AddShinyMusic();
return builder.Build();
}
}
<!-- Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Android 12 and below (API < 33) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
READ_MEDIA_AUDIO permission (audio files only).READ_EXTERNAL_STORAGE.<key>NSAppleMusicUsageDescription</key>
<string>This app needs access to your music library to browse and play your music.</string>
This key is mandatory. The app will crash on launch without it. No special entitlements are required.
Provides access to the device music library including permissions, querying, and file operations.
Task<PermissionStatus> RequestPermissionAsync();
Prompts the user for music library access. On Android, requests READ_MEDIA_AUDIO (API 33+) or READ_EXTERNAL_STORAGE (older). On iOS, calls MPMediaLibrary.RequestAuthorization.
Returns: PermissionStatus — Granted, Denied, Restricted (iOS only), or Unknown.
Task<PermissionStatus> CheckPermissionAsync();
Checks the current permission status without prompting the user.
Task<IReadOnlyList<MusicMetadata>> GetAllTracksAsync();
Returns all music tracks on the device. Permission must be granted first. On Android, queries MediaStore.Audio.Media with IsMusic != 0. On iOS, uses MPMediaQuery filtered to MPMediaType.Music. Only music is returned — no videos, ringtones, podcasts, or audiobooks.
Task<IReadOnlyList<MusicMetadata>> SearchTracksAsync(string query);
Searches tracks by title, artist, or album. Case-insensitive partial string matching.
Task<IReadOnlyList<GroupedCount<string>>> GetGenresAsync(MusicFilter? filter = null);
Returns all distinct, non-null genre names from the user's music library with track counts, sorted alphabetically. When a MusicFilter is provided, only tracks matching the filter criteria are considered for grouping. Permission must be granted first.
Task<IReadOnlyList<GroupedCount<int>>> GetYearsAsync(MusicFilter? filter = null);
Returns all distinct, non-zero release years from the user's music library with track counts, sorted in ascending order. When a MusicFilter is provided, only tracks matching the filter criteria are considered. On Android, uses MediaStore.Audio.Media.YEAR; on iOS, derives year from MPMediaItem.ReleaseDate.
Task<IReadOnlyList<GroupedCount<int>>> GetDecadesAsync(MusicFilter? filter = null);
Returns all distinct decades with track counts, sorted in ascending order. Each decade is its starting year (e.g., 1990 for the 1990s). When a MusicFilter is provided, only tracks matching the filter criteria are considered.
Task<IReadOnlyList<MusicMetadata>> GetTracksAsync(MusicFilter filter);
Returns tracks matching the specified filter criteria. All non-null filter properties are combined with AND logic. On Android, genre filtering queries via MediaStore.Audio.Genres.Members; year/decade/search use SQL WHERE clauses. On iOS, genre uses MPMediaQuery predicates; year/decade/search use LINQ filtering.
Task<IReadOnlyList<PlaylistInfo>> GetPlaylistsAsync();
Returns all playlists from the device music library with their song counts, sorted alphabetically by name. On Android, reads from MediaStore.Audio.Playlists. On iOS, reads from MPMediaQuery.PlaylistsQuery. Permission must be granted first.
Task<IReadOnlyList<MusicMetadata>> GetPlaylistTracksAsync(string playlistId);
Returns all tracks in the specified playlist, in playlist order. The playlistId is the platform-specific identifier returned by GetPlaylistsAsync. On Android, queries MediaStore.Audio.Playlists.Members. On iOS, retrieves tracks from the MPMediaPlaylist with the matching persistent ID.
Task<string?> GetAlbumArtPathAsync(string trackId);
Returns a file path or content URI to the album artwork image for the specified track. On Android, returns the content URI for the album art from MediaStore. On iOS, exports the MPMediaItem.Artwork image to a cached JPEG file and returns its path. Returns null if no artwork is available.
Task<bool> CopyTrackAsync(MusicMetadata track, string destinationPath);
Copies a music file to the specified path. Creates parent directories if needed. Returns false for DRM-protected tracks or on failure.
AVAssetExportSession in M4A format. DRM-protected Apple Music subscription tracks cannot be copied (AssetURL is null).Task<bool> HasStreamingSubscriptionAsync();
Checks whether the user has an active music streaming subscription that allows catalog playback. On iOS, this queries SKCloudServiceController for the MusicCatalogPlayback capability. On Android, this always returns false.
Controls playback of music files from the device library. Implements IDisposable.
On iOS, the player supports two modes:
AVAudioPlayer when ContentUri is available (purchased/synced tracks).MPMusicPlayerController.SystemMusicPlayer when StoreId is available (Apple Music subscription tracks).PlayAsync automatically selects the appropriate mode based on the track's properties.
Task PlayAsync(MusicMetadata track);
Stops any current track, loads the specified one, and begins playback. Throws InvalidOperationException if both ContentUri and StoreId are empty or the platform player fails.
Android.Media.MediaPlayer with content URIs.AVAudioPlayer with ipod-library:// asset URLs when ContentUri is available.MPMusicPlayerController.SystemMusicPlayer with the Apple Music catalog StoreId when ContentUri is empty but StoreId is available.void Pause(); // No effect if not Playing
void Resume(); // No effect if not Paused
void Stop(); // Stops and releases the current track
void Seek(TimeSpan position);
Seeks to the specified position. Android uses millisecond precision; iOS uses second precision.
float Volume { get; set; }
Gets or sets the playback volume. Value ranges from 0.0 (silent) to 1.0 (full volume). Default is 1.0. The value is automatically clamped to the valid range.
| Property | Type | Description |
|----------|------|-------------|
| State | PlaybackState | Current state: Stopped, Playing, or Paused |
| CurrentTrack | MusicMetadata? | Currently loaded track, or null if stopped |
| Position | TimeSpan | Current playback position (TimeSpan.Zero if no track) |
| Duration | TimeSpan | Total duration of current track (TimeSpan.Zero if no track) |
| Volume | float | Playback volume from 0.0 to 1.0 (default 1.0) |
| Event | Description |
|-------|-------------|
| StateChanged | Raised on state transitions (e.g., Playing -> Paused) |
| PlaybackCompleted | Raised when a track finishes naturally (not via Stop()) |
Provides lyrics for music tracks.
Task<LyricsResult?> GetLyricsAsync(MusicMetadata track);
Returns lyrics for the specified track, or null if no lyrics are available. The result may contain plain text lyrics, synchronized lyrics in LRC format, or both.
MPMediaItem.Lyrics for tracks in the local library (via the IMediaLibrary implementation which also implements ILyricsProvider).public record LyricsResult(string? PlainLyrics, string? SyncedLyrics);
| Property | Type | Description |
|----------|------|-------------|
| PlainLyrics | string? | Plain text (unsynchronized) lyrics, or null if unavailable |
| SyncedLyrics | string? | Synchronized lyrics in LRC format with timestamps, or null if unavailable |
Synced lyrics use the standard LRC format with timestamps:
[00:12.00]First line of lyrics
[00:17.50]Second line of lyrics
[00:23.10]Third line of lyrics
Each line is prefixed with [mm:ss.xx] indicating when the line should be displayed during playback.
public record MusicMetadata(
string Id,
string? Title,
string? Artist,
string? Album,
string? Genre,
TimeSpan Duration,
string? AlbumArtUri,
bool? IsExplicit,
string ContentUri,
string? StoreId = null,
int? Year = null
);
| Property | Description |
|----------|-------------|
| Id | Platform-specific unique ID. Android: MediaStore row ID. iOS: MPMediaItem persistent ID. |
| Title | Track title, or null if not available. |
| Artist | Artist or performer, or null if not available. |
| Album | Album name, or null if not available. |
| Genre | Genre, or null if unavailable. |
| Duration | Playback duration. |
| AlbumArtUri | Album art URI (Android only via MediaStore; null on iOS). |
| IsExplicit | Whether the track is marked as explicit content. iOS only via MPMediaItem.IsExplicitItem; always null on Android. |
| ContentUri | URI for playback/copy. Android: content:// URI. iOS: ipod-library:// asset URL. Empty string for DRM-protected Apple Music tracks — these cannot be played via AVAudioPlayer or copied. |
| StoreId | Optional Apple Music catalog ID (from PlayParams.Id). Enables streaming playback via MPMusicPlayerController on iOS. Always null on Android. |
| Year | Release year of the track, or null if not available. Android: MediaStore.Audio.Media.YEAR; iOS: derived from MPMediaItem.ReleaseDate. |
public record PlaylistInfo(string Id, string Name, int SongCount);
| Property | Description |
|----------|-------------|
| Id | Platform-specific unique identifier. Android: MediaStore playlist row ID. iOS: persistent ID. |
| Name | The display name of the playlist. |
| SongCount | The number of tracks in the playlist. |
Defines optional criteria for filtering music tracks. All specified properties are combined with AND logic. Used with GetTracksAsync, GetGenresAsync, GetYearsAsync, and GetDecadesAsync.
public class MusicFilter
{
public string? Genre { get; init; }
public int? Year { get; init; }
public int? Decade { get; init; }
public string? SearchQuery { get; init; }
}
| Property | Description |
|----------|-------------|
| Genre | Filter by genre name (case-insensitive match). |
| Year | Filter by exact release year. Takes precedence over Decade if both are set. |
| Decade | Filter by decade start year (e.g., 1990 for the 1990s). Ignored if Year is also set. |
| SearchQuery | Text search across title, artist, and album (case-insensitive, contains match). |
Returned by GetGenresAsync, GetYearsAsync, and GetDecadesAsync.
public record GroupedCount<T>(T Value, int Count);
| Property | Description |
|----------|-------------|
| Value | The grouped value (string for genres, int for years/decades). |
| Count | The number of tracks that belong to this group. |
| Value | Description |
|-------|-------------|
| Unknown | User has not been prompted yet |
| Denied | User denied access |
| Granted | User granted access |
| Restricted | iOS only — blocked by system policy (parental controls, MDM) |
| Value | Description |
|-------|-------------|
| Stopped | No track playing; player is idle |
| Playing | A track is actively playing |
| Paused | Playback is paused and can be resumed |
On iOS, Apple Music subscription tracks are DRM-protected. For these tracks:
MPMediaItem.AssetURL is nullMusicMetadata.ContentUri will be string.EmptyCopyTrackAsync will return falseHowever, if the track has a StoreId (Apple Music catalog ID), it can be played via MPMusicPlayerController.SystemMusicPlayer. The player automatically uses this path when StoreId is available.
| Track Source | ContentUri | StoreId | Playable | Copyable | |---|---|---|---|---| | iTunes purchases (DRM-free) | populated | may exist | AVAudioPlayer | yes | | Locally synced from computer | populated | no | AVAudioPlayer | yes | | Apple Music subscription | empty | populated | SystemMusicPlayer | no | | iTunes Match (cloud) | only if downloaded | may exist | depends | depends | | Android local files | always populated | no | yes | yes |
Use HasStreamingSubscriptionAsync() to determine if the user can play Apple Music catalog content:
var canStream = await _library.HasStreamingSubscriptionAsync();
if (canStream)
{
// User has an active Apple Music subscription
// Tracks with StoreId can be played via MPMusicPlayerController
}
On Android, this always returns false.
RequestPermissionAsync() before any query or playback operation.StoreId for streaming or ContentUri for local playback. If both are empty, the track cannot be played.IMediaLibrary and IMusicPlayer should be singletons in DI.IMusicPlayer implements IDisposable; call Dispose() or let the DI container handle it.Restricted on iOS — distinct from Denied; means system policy blocks access.AVAssetExportSession outputs M4A.HasStreamingSubscriptionAsync() — check before presenting streaming playback UI to the user.MusicFilter for combined queries — filter tracks by genre + year/decade in a single call rather than filtering in memory.GetGenresAsync(new MusicFilter { Decade = 1990 }) to find genres represented in the 90s.GetPlaylistsAsync and GetPlaylistTracksAsync — browse playlists and retrieve their contents in playlist order.GetAlbumArtPathAsync — retrieve album artwork as a cached file path for display in the UI.ILyricsProvider.GetLyricsAsync — fetch lyrics for a track. Check SyncedLyrics for timed LRC format, fall back to PlainLyrics for plain text.Volume on IMusicPlayer — control playback volume programmatically (0.0 to 1.0).// Fetch lyrics for a track
var lyrics = await _lyricsProvider.GetLyricsAsync(track);
if (lyrics != null)
{
if (!string.IsNullOrEmpty(lyrics.SyncedLyrics))
{
// Parse LRC format for synced display
// Format: [mm:ss.xx]Line of lyrics
foreach (var line in lyrics.SyncedLyrics.Split('\n'))
Console.WriteLine(line);
}
else if (!string.IsNullOrEmpty(lyrics.PlainLyrics))
{
// Display plain text lyrics
Console.WriteLine(lyrics.PlainLyrics);
}
}
// Get album artwork path
var artPath = await _library.GetAlbumArtPathAsync(track.Id);
if (artPath != null)
{
// Use as image source in MAUI
var imageSource = ImageSource.FromFile(artPath);
}
// All Rock tracks
var rockTracks = await library.GetTracksAsync(new MusicFilter { Genre = "Rock" });
// All tracks from the 1990s
var nineties = await library.GetTracksAsync(new MusicFilter { Decade = 1990 });
// Rock tracks from 1995
var rock95 = await library.GetTracksAsync(new MusicFilter { Genre = "Rock", Year = 1995 });
// Genres in the 2000s (with counts)
var genres2000s = await library.GetGenresAsync(new MusicFilter { Decade = 2000 });
// Years for Jazz (with counts)
var jazzYears = await library.GetYearsAsync(new MusicFilter { Genre = "Jazz" });
// Decades for Pop (with counts)
var popDecades = await library.GetDecadesAsync(new MusicFilter { Genre = "Pop" });
// Combined: genres matching "rock" search in the 1980s
var rock80s = await library.GetGenresAsync(new MusicFilter { Decade = 1980, SearchQuery = "rock" });
// Browse all 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);
devops
Guide for implementing push notifications in .NET MAUI apps using Shiny.Push (native FCM/APNs) and Shiny.Push.AzureNotificationHubs
tools
Cross-platform local notification management for .NET MAUI apps using Shiny, supporting scheduled, repeating, and geofence-triggered notifications with channels, badges, and interactive actions.
tools
GPS tracking, geofence monitoring, and motion activity recognition for .NET MAUI, iOS, and Android using Shiny.Locations
data-ai
Background job scheduling and execution for .NET MAUI (iOS/Android native OS schedulers) and in-process jobs for plain .NET, Linux, macOS, and Blazor WASM using Shiny.Jobs