mcpbeat Sign in

Polymorphic Load Media Entry Point Agent Skill

One generic "load this item and start playing" function normalizes several item types into a single internal shape, routes the queue-seeding strategy off a discriminator, and gates every pre-enqueue policy at that one place. Use when playback can start from many screens and a rule (content filter, dedup, analytics, resume-without-autoplay) keeps getting forgotten on one of the paths.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill polymorphic-load-media-entry-point

The instruction itself

3 sections, as written by the author

Polymorphic load-media entry point

Playback starts from a lot of places: a search row, a library row, a playlist, an album, a shared

link, a restored session. Each has its own item type and its own idea of what should be queued next.

Given one entry point per caller, any policy that must hold for *all* of them will be missing from

at least one.

Collapse them into a single function with two parameters that do the varying:

suspend fun <T> loadMediaItem(anyTrack: T, type: String, index: Int? = null) {
    val track = when (anyTrack) {
        is Track -> anyTrack
        is SongEntity -> anyTrack.toTrack()
        else -> return                       // unknown type: do nothing, touch nothing
    }
    // --- pre-enqueue policy: everything below this line assumes the item is allowed to play
    if (track.isExplicit && !settings.explicitContentEnabled()) { showToast(Explicit); return }
    songRepository.insertSong(track.toSongEntity())
    clearMediaItems()
    addMediaItem(track.toGenericMediaItem(), playWhenReady = type != RECOVER_TRACK_QUEUE)
    when (type) {
        SONG_CLICK, VIDEO_CLICK, SHARE       -> getRelated(track.trackId)   // seed from one item
        PLAYLIST_CLICK, ALBUM_CLICK, RADIO_CLICK -> loadPlaylistOrAlbum(index)
    }
}

The generic parameter normalizes content; the type discriminator routes strategy. They are

deliberately separate: two callers can pass the same item type and want different queues.

Traps

Policy gates must sit before the first player mutation, not after. Order is the whole point:

the content check returns *before* clearMediaItems(), so a blocked item leaves whatever is playing

untouched. Put the gate one line later and a rejected tap stops the music and shows a message — the

user loses their session to a rule that was supposed to change nothing.

Every side effect that must happen "whenever a track starts" belongs here, and only here.

Recording the item locally, back-filling its duration, dedup against what is already queued: each of

those, added at a call site, is a rule that holds on the screens someone remembered. Verify by

counting callers — the function should have many, and no caller should reach the player directly:

grep -rn "loadMediaItem(" --include='*.kt' .    # should list every playback origin
# in your UI module, nothing should reach the player directly — narrow the pattern to the
# player receiver so the handler's own delegating calls do not count as hits:
grep -rn "player\.addMediaItem(\|player\.setMediaItem(" --include='*.kt' <ui-module>/

(Quote the --include glob — unquoted, some shells expand it against the working directory and the

command fails before it searches anything.)

The type normalization needs a real else that does nothing. else -> return is not laziness;

it is the only correct answer for a caller that passed something the handler cannot represent.

Throwing turns a caller mistake into a stopped app, and a default of "treat it as a bare track" ships

an item with empty metadata into the queue.

A String discriminator has no exhaustiveness, and the failure is silent. A typo, or a new

origin added without a branch, falls through the when — the first item is enqueued and plays, and

the queue never fills, which reads as "the radio is broken" rather than "the constant is wrong".

Prefer a sealed type or an enum, and if it must stay a String, add an else branch that logs

loudly. Verify the set is closed by grepping the constants against the branches:

grep -rn "SONG_CLICK\|PLAYLIST_CLICK\|ALBUM_CLICK\|RADIO_CLICK\|SHARE\|RECOVER" --include='*.kt' .

Any constant that appears at a call site but not in the when is a dead origin.

Auto-play is derived from the discriminator, not passed as a flag. A restore path enqueues the

same item and must *not* start playing, so playWhenReady = type != RECOVER_TRACK_QUEUE keeps that

decision next to the branch that owns it. An extra boolean parameter would let a caller combine

"restore" with "play now", which is a state the app has no handling for.

The first item and the rest of the queue are seeded by different mechanisms. The tapped item is

added immediately so audio starts now; the queue fills asynchronously behind it. That means the

helper which adds items must self-start on the first one:

private fun addMediaItemNotSet(item: GenericMediaItem, index: Int? = null) {
    index?.let { player.addMediaItem(it, item) } ?: player.addMediaItem(item)
    if (player.mediaItemCount == 1) { player.prepare(); player.playWhenReady = true }
    updateNextPreviousTrackAvailability()
}

Without that count check, an item added into an empty engine sits there prepared-but-idle and the

tap appears to do nothing. With it applied unconditionally, every append restarts playback.

index means "position in the source", and one path re-reads it. For a playlist origin it is

which track the user tapped; the seeding path re-catalogs around it — moving the already-enqueued

item to that position and skipping it during the re-add, since it is already in the player. An

index that does not point at the item just enqueued shifts

the whole queue against the engine — the same failure the restore path guards against by putting the

track at the front when the saved queue does not contain it.

Being suspend is part of the contract. The gate reads a setting, and the normalization may hit

storage. Making it fire-and-forget internally would let a caller's own coroutine cancellation race

the gate, so the policy check would sometimes not complete before the item was enqueued.

Verifying it

Add a temporary log at the top of the entry point and exercise every origin the app has — each tap,

each long-press menu, each share link, plus a cold restore. Every one should print exactly once. An

origin that plays without printing is a call site that reached the player directly, and it is where

the next forgotten rule will land.

How to use it

Copy the folder

Take maxrave-dev/polymorphic-load-media-entry-point from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.