diff --git a/src/c/jellyPod.c b/src/c/jellyPod.c index 1db255f..d54f182 100644 --- a/src/c/jellyPod.c +++ b/src/c/jellyPod.c @@ -142,6 +142,29 @@ static void to_pkjs_play_from_track(uint16_t seqId) { } } +static void to_pkjs_shuffle_in_scope(NavScope scope, uint16_t *subscope_index, bool use_groups) { + if (!s_pkjs_ready) { + return; + } + + DictionaryIterator *out; + AppMessageResult result = app_message_outbox_begin(&out); + if (result != APP_MSG_OK) { + return; + } + const char *subscope = subscope_index ? s_current_menu_vanity[*subscope_index] : ""; + if (subscope_index) { + dict_write_cstring(out, MESSAGE_KEY_OUT_MENU_NAV_SUBSCOPE, subscope); + } + dict_write_uint8(out, MESSAGE_KEY_BIDIR_MENU_NAV_SCOPE, scope); + dict_write_uint8(out, MESSAGE_KEY_OUT_SHUFFLE_MODE, use_groups ? 2 : 1); + + result = app_message_outbox_send(); + if (result != APP_MSG_OK) { + return; + } +} + static void to_pkjs_move_deeper(NavScope scope, uint16_t *subscope_index) { if (!s_pkjs_ready) { return; @@ -236,10 +259,10 @@ static void menu_select_click(struct MenuLayer *menu_layer, MenuIndex *clicked_i case NAV_SCOPE_MAIN: switch (clicked_index->row) { case 0: - // TODO tell PKJS to shuffle all songs + to_pkjs_shuffle_in_scope(s_current_menu_scope, NULL, false); break; case 1: - // TODO tell PKJS to shuffle artists + to_pkjs_shuffle_in_scope(s_current_menu_scope, NULL, true); break; case 2: to_pkjs_move_deeper(NAV_SCOPE_ARTISTS, NULL); diff --git a/src/pkjs/index.js b/src/pkjs/index.js index 0071e55..cc19b3c 100644 --- a/src/pkjs/index.js +++ b/src/pkjs/index.js @@ -14,7 +14,8 @@ var NavScope = Object.freeze({ var cfg = JSON.parse(localStorage.getItem('clay-settings')); var currentArtist = null; -////////////////////// GENERAL API VARS ////////////////////// +////////////////////// GENERAL VARS ////////////////////// +var QUEUE_SIZE = 100; var USER_ID_CACHE_KEY = 'user-id'; var jfSessionId; @@ -229,6 +230,41 @@ function ringSlice(arr, start, end) { return Array.from({ length: count }, (_, k) => arr[(((start + k) % n) + n) % n]); } +// Fisher-Yates shuffles a copy of an array +function shuffledCopy(arr) { + const result = arr.slice(); + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const tmp = result[i]; + result[i] = result[j]; + result[j] = tmp; + } + return result; +} + +// flattens an album to tracks map into jfIds +function albumMapJfIds(albumToTracksMap) { + const jfIds = []; + for (const tracks of albumToTracksMap.values()) { + for (const track of tracks) { + jfIds.push(track.JfId); + } + } + return jfIds; +} + +// shuffle the order of whole groups while keeping each group's tracks together +function groupedShuffle(groups, limit) { + const result = []; + for (const group of shuffledCopy(groups)) { + for (const jfId of group) { + result.push(jfId); + if (result.length >= limit) { return result; } + } + } + return result; +} + Pebble.addEventListener("appmessage", function (dict) { //console.log("payload", JSON.stringify(dict.payload)); //console.log("expected NAV_SCOPE key", keys.BIDIR_MENU_NAV_SCOPE); @@ -236,12 +272,69 @@ Pebble.addEventListener("appmessage", function (dict) { const scope = payloadGet(dict.payload, "BIDIR_MENU_NAV_SCOPE"); const subscope = payloadGet(dict.payload, "OUT_MENU_NAV_SUBSCOPE"); const seqId = payloadGet(dict.payload, "OUT_PLAY_SEQ_ID"); + const shuffleMode = payloadGet(dict.payload, "OUT_SHUFFLE_MODE"); if (seqId !== undefined) { - callAPI("POST", cfg.PRIV_BASE_URL + "/Sessions/" + jfSessionId + "/Playing?playCommand=PlayNow&itemIds=" + ringSlice(allJfIds, seqId, seqId+99), null); + callAPI("POST", cfg.PRIV_BASE_URL + "/Sessions/" + jfSessionId + "/Playing?playCommand=PlayNow&itemIds=" + ringSlice(allJfIds, seqId, seqId+QUEUE_SIZE), null); } if (scope !== undefined) { + // scope shuffle + if (shuffleMode) { + // build a shuffled queue of jfIds for the current scope + let queuedJfIds = []; + switch (scope) { + case NavScope.NAV_SCOPE_MAIN: + if (shuffleMode == 1) { + // choose 100 random jfIds from the entire library (truly random order) + queuedJfIds = shuffledCopy(allJfIds).slice(0, QUEUE_SIZE); + } else { + // choose 100 random jfIds from the entire library (grouped by artist) + const artistGroups = []; + for (const artistToAlbumsMap of lookupMap.values()) { + for (const albumToTracksMap of artistToAlbumsMap.values()) { + artistGroups.push(albumMapJfIds(albumToTracksMap)); + } + } + queuedJfIds = groupedShuffle(artistGroups, QUEUE_SIZE); + } + break; + case NavScope.NAV_SCOPE_ALBUMS: { + const artistToAlbumsMap = lookupMap.get(Array.from(subscope)[0].toUpperCase()); + const albumToTracksMap = artistToAlbumsMap ? artistToAlbumsMap.get(subscope) : null; + if (albumToTracksMap) { + if (shuffleMode == 1) { + // choose 100 random jfIds from the current artist (truly random order) + queuedJfIds = shuffledCopy(albumMapJfIds(albumToTracksMap)).slice(0, QUEUE_SIZE); + } else { + // choose 100 random jfIds from the current artist (grouped by album) + const albumGroups = []; + for (const tracks of albumToTracksMap.values()) { + albumGroups.push(tracks.map((track) => track.JfId)); + } + queuedJfIds = groupedShuffle(albumGroups, QUEUE_SIZE); + } + } + break; + } + case NavScope.NAV_SCOPE_ALBUM_TRACKS: { + const artistToAlbumsMap = lookupMap.get(Array.from(currentArtist)[0].toUpperCase()); + const albumToTracksMap = artistToAlbumsMap ? artistToAlbumsMap.get(currentArtist) : null; + const tracks = albumToTracksMap ? albumToTracksMap.get(subscope) : null; + if (tracks) { + // randomize the order of jfIds from the current album + queuedJfIds = shuffledCopy(tracks.map((track) => track.JfId)); + } + break; + } + } + if (queuedJfIds.length > 0) { + callAPI("POST", cfg.PRIV_BASE_URL + "/Sessions/" + jfSessionId + "/Playing?playCommand=PlayNow&itemIds=" + queuedJfIds.join(','), null); + } + return; // no menu navigation + } + + // navigation let vanityList; let seqIdList = []; switch (scope) {