mcpbeat Sign in

Custom Shuffle Order Agent Skill

Replacing a media engine's default shuffle order so that tracks added mid-playback land contiguously after the current one instead of being scattered through the rest of the queue. Use when "play next" or an appended continuation page ends up in random positions while shuffle is on, or when writing any custom shuffle order and needing the insert/remove/clone contract to stay consistent.

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 custom-shuffle-order

The instruction itself

3 sections, as written by the author

Custom shuffle order

A media engine keeps items in timeline order and plays them through a shuffle order: a

permutation array shuffled, where shuffled[playPosition] = timelineIndex. The engine asks it for

next/previous and for the first/last entry, and asks it to *clone itself* whenever the queue changes.

The default implementation shuffles newly inserted items into random positions across the whole

remaining order. That is defensible for "shuffle the album" and wrong for everything else: a user

who hits "play next" wants the track next, and a queue that grows by appending a continuation page

wants that page to stay in one block. Replacing it is a small class:

class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder {
    // reverse map: indexInShuffled[timelineIndex] = playPosition
    private val indexInShuffled = IntArray(shuffled.size).also {
        for (i in shuffled.indices) it[shuffled[i]] = i
    }

    override fun getNextIndex(index: Int): Int {
        val next = indexInShuffled[index] + 1
        return if (next < shuffled.size) shuffled[next] else INDEX_UNSET
    }
}

Two arrays, always built together: the forward map answers "what plays at position N", the reverse

map answers "where in the play order does timeline item N sit". Every operation below has to leave

both consistent.

Traps

The reverse map is derived, never edited in place. Every constructor path must rebuild

indexInShuffled from shuffled — deriving it in the property initialiser, as above (an init

block does the same job), makes that structural rather than a rule someone has to remember. An insert or remove that patches only the forward array

leaves the reverse one describing the previous queue, and the symptom is not a failure: playback

simply jumps to an unrelated track at the next boundary.

Insert has to do two different things at once, and it is easy to do only one. Existing entries

whose *timeline* index sits at or after the insertion point shift up by the insert count; separately,

the new entries must be placed at a *play* position next to the pivot. Both, or the order is

corrupt:

override fun cloneAndInsert(insertionIndex: Int, insertionCount: Int): ShuffleOrder {
    val pivot = if (insertionIndex < shuffled.size) indexInShuffled[insertionIndex]
                else indexInShuffled.size
    val out = IntArray(shuffled.size + insertionCount)
    for (i in shuffled.indices) {
        val timelineIndex = shuffled[i].let { if (it > insertionIndex) it + insertionCount else it }
        if (i <= pivot) out[i] = timelineIndex else out[i + insertionCount] = timelineIndex
    }
    for (i in 0 until insertionCount) {
        if (insertionIndex < shuffled.size) {
            out[pivot + i + 1] = insertionIndex + i + 1   // insert after the pivot entry
        } else {
            out[pivot + i] = insertionIndex + i           // append past the end: no pivot entry
        }
    }
    return BetterShuffleOrder(out)
}

Both branches on insertionIndex < shuffled.size are load-bearing: appending past the end has no

pivot entry to read, so the new items start at pivot rather than pivot + 1 and their timeline

indices are not shifted. Dropping the append branch either leaves an uninitialised 0 in the middle

of the order — which plays the first track again at a random moment — or writes one slot past the

end of the array.

Remove has to renumber survivors, not just drop entries. Timeline indices above the removed

range all shift down by the removed count. Dropping the entries and leaving the rest as-is produces

an order that points past the end of a shrinking queue.

newShuffled[i - foundSoFar] =
    if (shuffled[i] >= indexFrom) shuffled[i] - removedCount else shuffled[i]

These are cloneAnd*, not insert/remove. The engine holds the old instance while building

the new timeline and may discard the result. Mutating this and returning it appears to work until

a queue edit is rolled back, and then the order describes a queue that never existed. Return a new

instance from every one of them, including cloneAndClear.

The start index must be swapped to the front, not just included. Shuffling a queue while a track

plays must keep that track first, or turning shuffle on restarts playback from something else:

if (startIndex != -1) {
    val pos = shuffled.indexOf(startIndex)
    shuffled[0] = shuffled[pos].also { shuffled[pos] = shuffled[0] }
}

Generate the permutation with the inside-out variant, not List.shuffle() on a copy. The

inside-out Fisher–Yates fills and permutes in one pass over 0 until length, which keeps the

allocation to the single output array:

for (i in 0 until length) {
    val swap = (0..i).random()
    shuffled[i] = shuffled[swap]
    shuffled[swap] = i
}

Every entry point must survive an empty order. getFirstIndex, getLastIndex and

cloneAndInsert are all reachable with shuffled.size == 0 — a queue cleared while shuffle stays

on. Each needs an explicit branch (INDEX_UNSET, or a fresh order built from the insert count);

an unguarded shuffled[0] there stops playback with an out-of-range read.

Un-shuffling does not restore your custom insert positions. The engine reverts to timeline order

when shuffle is turned off, so a track that was "played next" reappears wherever it was inserted in

the timeline. Deciding whether that is acceptable is a product call — it is not something the

shuffle order can fix, because timeline order is the other authority.

Verifying it

Log the array itself, not the behaviour: print shuffled after each clone and check by hand that

it is a permutation of 0 until size (no repeats, no gaps) with the inserted block contiguous right

after the current play position. A corrupted order is almost always a repeated or missing value, and

that is visible in one printed line long before it is visible as a skipped track.


*Provenance: the design shown here follows the GPL-3.0 BetterShuffleOrder from the Auxio project

(Alexander Capehart). Reproducing the code — rather than the pattern — in your own project carries

that license and its attribution with it.*

How to use it

Copy the folder

Take maxrave-dev/custom-shuffle-order 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.