Music-Player icon indicating copy to clipboard operation
Music-Player copied to clipboard

Album-Shuffle Mode

Open bigboipete opened this issue 1 year ago • 7 comments

Checklist

  • [X] I made sure that there are no existing issues - open or closed - to which I could contribute my information.
  • [X] I made sure that there are no existing discussions - open or closed - to which I could contribute my information.
  • [X] I have read the FAQs inside the app (Menu -> About -> FAQs) and my problem isn't listed.
  • [X] I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise.
  • [X] This issue contains only one feature request.
  • [X] I have read and understood the contribution guidelines.
  • [ ] I optionally donated to support the Fossify mission.

Feature description

My suggestion is an album shuffle mode, that considers full albums being the items to shuffle. This mode should play all songs of one album in it's orignial (tagged) song order and then switch to anothor random album.

As a fallback for missing track-numbers, alphabetical songorder could be chosen. If it's a mixture of both, it could be all available track numberes ordered first followed by the remaining album tracks in alphabetical order.

To expand this feature a bit more, there could be an option in the settings how to handle albums spanning over two or more disks, to keep each disk of an album sperated or consider all as one entity. To ease implementation efforts and looking at my personal preference, I would rather go with all disks belong to one album - no split.

Why do you want this feature?

If you like the concept of an album as a complete collection of songs and as one entity of an artits's output, it is currently not possible to apply this to a shuffle mode.

Additional information

No response

bigboipete avatar May 24 '24 11:05 bigboipete

Agreed, this is an important one to me. Can anyone with a high-level view of how the app works let us know what needs to happen? I can do some entry level coding but it'd take ages to try to understand the overall structure of the app

edit: I started looking into this today. There's a function Player.shuffledMediaItemsIndices in musicplayer/extensions/Player.kt that seems promising. I think when shuffle mode is started, it uses an android builtin media player library to shuffle the list of track indices. Other functions then parse the indices into the actual tracks to play. If I can crowbar that into logic that returns indices for album-shuffled tracks, then shuffle mode should have the album-shuffling behavior we want. I haven't started digging into how to actually accomplish that, though.

ttshaw1 avatar Jun 03 '25 01:06 ttshaw1

I modified the code and got a hacky album shuffle working well enough for me. This in no way is good code!

**
     * Executes [callback] with current track as quickly as possible and then proceeds to load the complete queue with all tracks.
     */
    fun getQueuedTracksLazily(callback: (tracks: List<Track>, startIndex: Int, startPositionMs: Long) -> Unit) {
        ensureBackgroundThread {
            var queueItems = context.queueDAO.getAll()
            if (queueItems.isEmpty()) {
                initQueue()
                queueItems = context.queueDAO.getAll()
            }

            val currentItem = context.queueDAO.getCurrent()
            if (currentItem == null) {
                callback(emptyList(), 0, 0)
                return@ensureBackgroundThread
            }

            val currentTrack = getTrack(currentItem.trackId)
            if (currentTrack == null) {
                callback(emptyList(), 0, 0)
                return@ensureBackgroundThread
            }

            // immediately return the current track.
            val startPositionMs = currentItem.lastPosition.seconds.inWholeMilliseconds
            callback(listOf(currentTrack), 0, startPositionMs)


            /* original
            // return the rest of the queued tracks.
            val queuedTracks = getQueuedTracks(queueItems)
            val currentIndex = queuedTracks.indexOfFirstOrNull { it.mediaStoreId == currentTrack.mediaStoreId } ?: 0
            callback(queuedTracks, currentIndex, startPositionMs)
            */

            //from chat gpt
            val queuedTracks = getQueuedTracks(queueItems)

            // Group tracks by album (using album ID or title)
            val albums: List<List<Track>> = queuedTracks.groupBy { it.albumId /* or it.album */ }
                .values
                .map { it.sortedBy { track -> track.trackId } } // sort tracks within album

            // Shuffle album order
            val shuffledAlbums = albums.shuffled()

            // Flatten back to a single list
            val shuffledTracks = shuffledAlbums.flatten()

            // Find current index in new list
            val currentIndex = shuffledTracks.indexOfFirstOrNull { it.mediaStoreId == currentTrack.mediaStoreId } ?: 0

            Log.d("AlbumShuffle", "Final shuffled queue:")
            shuffledTracks.take(100).forEachIndexed { index, track ->
                Log.d("AlbumShuffle", "[$index] ${track.title} from album ${track.album} (trackId: ${track.trackId})")
            }


            // Return shuffled track list to player
            callback(shuffledTracks, currentIndex, startPositionMs)
        }
    }

To get a fresh album shuffle, it seems like I need to have every song in my UI queue, then empty the queue. Sometimes that crashes the app, sometimes I have to force close it. When it's back I can look at the queue and it's got a new album shuffle - but if I pick a track or album or anything, it'll modify the queue and I have to repeat the procedure to get a new album shuffle.

My mental model is that the app works about like this:

  1. Something calls Player.maybePreparePlayer . Haven't checked what, probably happens on app startup or first time you want to play something, maybe in the future as well
  2. In that function, getQueuedTracksLazily is called. Maybe only on the first call of maybePreparePlayer
  3. there's a queueDAO object that contains the queue. getQueuedTracksLazily gets the queue from it
  4. getQueuedTracksLazily returns the first track in the queue, then the rest of the queue (lazy loading). maybePreparePlayer now has the queue
  5. maybePreparePlayer passes the tracks to prepareUsingTracks, which uses setMediaItems to pass them to android's exoplayer library, where they becomes individual Windows in a Timeline
  6. functions like shuffledMediaItemsIndices ask exoplayer for the indices of the tracks in the Timeline, letting exoplayer handle the shuffling in that case

So there are a ton of places album shuffling could drop in. I think the cleanest would perhaps be to make each album a Window. Or maybe we could override exoplayer's shuffling function?

ttshaw1 avatar Jun 14 '25 01:06 ttshaw1

Is there any app that has this functionality?

naveensingh avatar Jun 14 '25 07:06 naveensingh

PowerAmp does. "Categories"" refers to Albums, Genres, Years, Artists, etc depending on how the content was listed when you started playing. But it's closed source, so no way to see how they do it.

Image

ttshaw1 avatar Jun 14 '25 11:06 ttshaw1

Musicbee has it. But it's not an Android app. I currently don't know any player on Android that can do it (besides PowerAmp, thx to @ttshaw1). That's why I filed the feature request.

bigboipete avatar Jun 14 '25 13:06 bigboipete

I know of 'Oto Music', "Rocket Player', and 'Symfonium' that have this feature.

dejan-deletic avatar Jun 17 '25 12:06 dejan-deletic

I'm thinking this could be a hidden dialog that appears when the shuffle button is long-pressed. Example options:

  • Shuffle tracks
  • Shuffle albums

naveensingh avatar Jul 06 '25 03:07 naveensingh