mirror of
https://git.wetfence.xyz/RandyTheSilly/jellyPod.git
synced 2026-10-26 15:29:57 -04:00
Significant Jellyfin API work on the PKJS side
This commit is contained in:
1 parent
317241d294
commit
79045d8b4b
5 files changed
+191
-46
No files matched your search
@@ -3,3 +3,4 @@
|
||||
/.lock-waf*
|
||||
/package-lock.json
|
||||
/node_modules
|
||||
/*.json
|
||||
@@ -1,12 +1,12 @@
|
||||
# JellyPod
|
||||
# jellyPod
|
||||
Control Jellyfin music playback from your Pebble with an iPod-like interface.
|
||||
|
||||
# Supported Phone Clients
|
||||
JellyPod requires a Jellyfin client to be running on your phone to handle playback. Not all clients support all features of JellyPod. Tested clients are detailed below.
|
||||
jellyPod requires a Jellyfin client to be running on your phone to handle playback. Not all clients support all features of jellyPod. Tested clients are detailed below.
|
||||
## Legend
|
||||
- ✅ = verified (supports all features of JellyPod and prefers offline playback of local media over online streaming)
|
||||
- ✅ = verified (supports all features of jellyPod and prefers offline playback of local media over online streaming)
|
||||
- ⚠️ = usable (functional with missing features/bugs detailed below)
|
||||
- ⛔ = unsupported (does not work well or at all with JellyPod)
|
||||
- ⛔ = unsupported (does not work well or at all with jellyPod)
|
||||
## iOS
|
||||
- ✅ [Finamp Beta/Redesign](https://github.com/finamp-app/finamp/tree/redesign) (last tested release: 1.0.1)
|
||||
|
||||
@@ -15,7 +15,7 @@ JellyPod requires a Jellyfin client to be running on your phone to handle playba
|
||||
- Your music continues to play in order even after you exhaust the tracks on the selected album and by the current artist
|
||||
- The next album is chosen by release date and the next artist is chosen alphanumerically
|
||||
- Multi-client/device support
|
||||
- JellyPod is not just a remote for your phone
|
||||
- jellyPod is not just a remote for your phone
|
||||
- Determines which client to control based on the most recently active session under your Jellyfin user
|
||||
- Classic design
|
||||
- Inspired by the 4th generation iPod
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
"@rebble/clay": "^1.0.10"
|
||||
},
|
||||
"pebble": {
|
||||
"displayName": "JellyPod",
|
||||
"displayName": "jellyPod",
|
||||
"uuid": "b135c5a1-ddeb-4278-9da5-3ac68b718f0c",
|
||||
"sdkVersion": "3",
|
||||
"enableMultiJS": true,
|
||||
@@ -40,7 +40,7 @@
|
||||
"IN_VANITY_LIST",
|
||||
"IN_ID_LIST",
|
||||
"OUT_MENU_NAV_SCOPE",
|
||||
"OUT_TRACK_ID",
|
||||
"OUT_JFID",
|
||||
"OUT_SHUFFLE_MODE"
|
||||
],
|
||||
"resources": {
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
#include <pebble.h>
|
||||
|
||||
typedef enum {
|
||||
MENU_NAV_DISPLAY_MAIN = 0,
|
||||
MENU_NAV_DISPLAY_ARTISTS = 1,
|
||||
MENU_NAV_DISPLAY_ALBUMS = 2,
|
||||
MENU_NAV_DISPLAY_ALBUM_TRACKS = 3,
|
||||
MENU_NAV_DISPLAY_PLAYLISTS = 4,
|
||||
MENU_NAV_DISPLAY_PLAYLIST_TRACKS = 5,
|
||||
} MenuNavDisplay;
|
||||
NAV_SCOPE_MAIN = 0,
|
||||
NAV_SCOPE_ALPHA = 1,
|
||||
NAV_SCOPE_ARTISTS = 2,
|
||||
NAV_SCOPE_ALBUMS = 3,
|
||||
NAV_SCOPE_ALBUM_TRACKS = 4,
|
||||
NAV_SCOPE_PLAYLISTS = 5,
|
||||
NAV_SCOPE_PLAYLIST_TRACKS = 6,
|
||||
NAV_SCOPE_NOW_PLAYING = 7
|
||||
} NavScope;
|
||||
|
||||
// layer-age
|
||||
static Window *s_main_window;
|
||||
@@ -18,7 +20,8 @@ static Layer *s_divider_layer;
|
||||
static MenuLayer *s_menu_layer;
|
||||
|
||||
// globals
|
||||
static MenuNavDisplay s_current_menu_display = MENU_NAV_DISPLAY_MAIN;
|
||||
static bool s_pkjs_ready = false;
|
||||
static NavScope s_current_menu_scope = NAV_SCOPE_MAIN;
|
||||
|
||||
// resources
|
||||
static GFont s_font_chicago;
|
||||
@@ -39,34 +42,99 @@ static GFont s_font_chicago;
|
||||
static const GRect s_menu_grect = GRect(MARGIN_X, BAR_HEIGHT + MARGIN_Y, PBL_DISPLAY_WIDTH - (MARGIN_X * 2), PBL_DISPLAY_HEIGHT - (MARGIN_Y * 2));
|
||||
static const char *s_main_menu_options[3] = {"Artists", "Playlists", "Now Playing"};
|
||||
|
||||
////////////////////// PKJS //////////////////////
|
||||
|
||||
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
||||
Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_PKJS_READY);
|
||||
if (ready_tuple) {
|
||||
s_pkjs_ready = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void to_pkjs_scope_change(NavScope scope, const char *jfId, bool shuffleInScope) {
|
||||
if (!s_pkjs_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
// send scope to return data for (PKJS side should track last scope and only send new data if the new scope differs)
|
||||
DictionaryIterator *out;
|
||||
AppMessageResult result = app_message_outbox_begin(&out);
|
||||
if (result != APP_MSG_OK) {
|
||||
return;
|
||||
}
|
||||
dict_write_uint8(out, MESSAGE_KEY_OUT_MENU_NAV_SCOPE, scope);
|
||||
|
||||
if (shuffleInScope) {
|
||||
// tell PKJS to shuffle if a shuffle option was clicked
|
||||
dict_write_uint8(out, MESSAGE_KEY_OUT_SHUFFLE_MODE, shuffleInScope); // if true, PKJS side shuffles IDs in scope and passes them to JF API
|
||||
} else if (jfId) {
|
||||
// otherwise, send the selected jfId, if present;
|
||||
// for artists/albums/playlists, determines next data set returned to C;
|
||||
// for tracks, starts sequential playback (sequential relative to entire library unless scope is playlist)
|
||||
dict_write_cstring(out, MESSAGE_KEY_OUT_JFID, jfId);
|
||||
}
|
||||
|
||||
result = app_message_outbox_send();
|
||||
if (result != APP_MSG_OK) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static uint16_t menu_get_num_rows(MenuLayer *menu_layer, uint16_t section_index, void *context) {
|
||||
switch (s_current_menu_display) {
|
||||
case MENU_NAV_DISPLAY_MAIN:
|
||||
switch (s_current_menu_scope) {
|
||||
case NAV_SCOPE_MAIN:
|
||||
return 3;
|
||||
case MENU_NAV_DISPLAY_ARTISTS:
|
||||
case MENU_NAV_DISPLAY_ALBUMS:
|
||||
case MENU_NAV_DISPLAY_ALBUM_TRACKS:
|
||||
case MENU_NAV_DISPLAY_PLAYLISTS:
|
||||
case NAV_SCOPE_ALPHA:
|
||||
case NAV_SCOPE_ARTISTS:
|
||||
case NAV_SCOPE_ALBUMS:
|
||||
case NAV_SCOPE_ALBUM_TRACKS:
|
||||
case NAV_SCOPE_PLAYLISTS:
|
||||
default:
|
||||
return 99;
|
||||
}
|
||||
}
|
||||
|
||||
static void menu_draw_row(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index, void *context) {
|
||||
switch (s_current_menu_display) {
|
||||
case MENU_NAV_DISPLAY_MAIN:
|
||||
switch (s_current_menu_scope) {
|
||||
case NAV_SCOPE_MAIN:
|
||||
custom_menu_cell_title_draw(ctx, cell_layer, s_main_menu_options[cell_index->row], NULL, s_font_chicago, MARGIN_X);
|
||||
break;
|
||||
case MENU_NAV_DISPLAY_ARTISTS:
|
||||
case MENU_NAV_DISPLAY_ALBUMS:
|
||||
case MENU_NAV_DISPLAY_ALBUM_TRACKS:
|
||||
case MENU_NAV_DISPLAY_PLAYLISTS:
|
||||
case NAV_SCOPE_ALPHA:
|
||||
case NAV_SCOPE_ARTISTS:
|
||||
case NAV_SCOPE_ALBUMS:
|
||||
case NAV_SCOPE_ALBUM_TRACKS:
|
||||
case NAV_SCOPE_PLAYLISTS:
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void menu_selection_changed(struct MenuLayer *menu_layer, MenuIndex new_index, MenuIndex old_index, void *callback_context) {
|
||||
static void menu_select_click(struct MenuLayer *menu_layer, MenuIndex *clicked_index, void *context) {
|
||||
switch (s_current_menu_scope) {
|
||||
case NAV_SCOPE_MAIN:
|
||||
switch (clicked_index->row) {
|
||||
case 0:
|
||||
to_pkjs_scope_change(NAV_SCOPE_ALPHA, NULL, false);
|
||||
break;
|
||||
case 1:
|
||||
to_pkjs_scope_change(NAV_SCOPE_PLAYLISTS, NULL, false);
|
||||
break;
|
||||
default:
|
||||
to_pkjs_scope_change(NAV_SCOPE_NOW_PLAYING, NULL, false); // TODO
|
||||
}
|
||||
return;
|
||||
case NAV_SCOPE_ALPHA:
|
||||
case NAV_SCOPE_ARTISTS:
|
||||
case NAV_SCOPE_ALBUMS:
|
||||
case NAV_SCOPE_ALBUM_TRACKS:
|
||||
case NAV_SCOPE_PLAYLISTS:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void menu_selection_changed(struct MenuLayer *menu_layer, MenuIndex new_index, MenuIndex old_index, void *context) {
|
||||
speaker_play_tone(200, 10, 80, SpeakerWaveformSine);
|
||||
}
|
||||
|
||||
@@ -79,7 +147,7 @@ static void main_window_load() {
|
||||
Layer *main_window_layer = window_get_root_layer(s_main_window);
|
||||
|
||||
s_menu_layer = menu_layer_create(s_menu_grect);
|
||||
menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){.get_num_rows = menu_get_num_rows, .draw_row = menu_draw_row, .selection_changed = menu_selection_changed});
|
||||
menu_layer_set_callbacks(s_menu_layer, NULL, (MenuLayerCallbacks){.get_num_rows = menu_get_num_rows, .draw_row = menu_draw_row, .select_click = menu_select_click, .selection_changed = menu_selection_changed});
|
||||
menu_layer_pad_bottom_enable(s_menu_layer, false);
|
||||
menu_layer_set_click_config_onto_window(s_menu_layer, s_main_window);
|
||||
#ifdef PBL_ROUND
|
||||
@@ -88,7 +156,7 @@ static void main_window_load() {
|
||||
layer_add_child(main_window_layer, menu_layer_get_layer(s_menu_layer));
|
||||
|
||||
s_status_bar_layer = custom_status_bar_layer_create(BAR_HEIGHT, GColorWhite, 15);
|
||||
custom_status_bar_layer_set_text(s_status_bar_layer, CSB_TEXT_CENTER, "pebble");
|
||||
custom_status_bar_layer_set_text(s_status_bar_layer, CSB_TEXT_CENTER, "jellyPod");
|
||||
custom_status_bar_layer_set_text_font(s_status_bar_layer, CSB_TEXT_CENTER, s_font_chicago);
|
||||
#ifdef PBL_RECT
|
||||
custom_status_bar_layer_set_text(s_status_bar_layer, CSB_TEXT_LEFT, " | |");
|
||||
@@ -107,15 +175,6 @@ static void main_window_unload() {
|
||||
menu_layer_destroy(s_menu_layer);
|
||||
}
|
||||
|
||||
////////////////////// PKJS //////////////////////
|
||||
|
||||
static void inbox_received_handler(DictionaryIterator *iter, void *context) {
|
||||
Tuple *ready_tuple = dict_find(iter, MESSAGE_KEY_PKJS_READY);
|
||||
if (ready_tuple) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void init() {
|
||||
// initialize pkjs
|
||||
app_message_register_inbox_received(inbox_received_handler);
|
||||
+91
-6
@@ -2,32 +2,117 @@ var Clay = require('@rebble/clay');
|
||||
var clayConfig = require('./config');
|
||||
new Clay(clayConfig);
|
||||
|
||||
var cfg = JSON.parse(localStorage.getItem('clay-settings'));
|
||||
|
||||
Pebble.addEventListener("ready", function () {
|
||||
// store cache of library info from Jellyfin
|
||||
if (cfg.PRIV_BASE_URL && cfg.PRIV_API_KEY && cfg.PRIV_USERNAME) {
|
||||
// get userId
|
||||
callAPI("GET", cfg.PRIV_BASE_URL + "/Users", function (userIdResp) {
|
||||
if (userIdResp === null) { return; }
|
||||
const userIdObj = JSON.parse(userIdResp);
|
||||
let userId;
|
||||
for (const user of userIdObj) {
|
||||
if (user.Name === cfg.PRIV_USERNAME) {
|
||||
userId = user.Id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// get library
|
||||
callAPI("GET", cfg.PRIV_BASE_URL + "/Items?userId=" + userId + "&recursive=true&includeItemTypes=Audio&sortBy=AlbumArtist,Artist,ProductionYear,Album,IndexNumber,Name'", function (libResp) {
|
||||
if (libResp === null) { return; }
|
||||
const libObj = JSON.parse(libResp);
|
||||
// extraction output:
|
||||
// a map of artists to arrays of albums
|
||||
// a map of albums to arrays of contained tracks
|
||||
|
||||
// extract alphanumerical list of artists
|
||||
let artists = [];
|
||||
let lastArtist = "";
|
||||
let unknownFound = false;
|
||||
for (const track of libObj.Items) {
|
||||
if (track.AlbumArtist) {
|
||||
if (track.AlbumArtist != lastArtist) {
|
||||
artists.push(track.AlbumArtist);
|
||||
lastArtist = track.AlbumArtist;
|
||||
}
|
||||
} else if (track.Artists.length > 0) {
|
||||
if (track.Artists[0] != lastArtist) {
|
||||
artists.push(track.Artists[0]);
|
||||
lastArtist = track.Artists[0];
|
||||
}
|
||||
} else if (!unknownFound) {
|
||||
unknownFound = true;
|
||||
}
|
||||
}
|
||||
if (unknownFound) {
|
||||
artists.push("unknown artist");
|
||||
}
|
||||
console.log(artists);
|
||||
|
||||
// sort artists into arrays mapped to first characters
|
||||
const charMap = new Map();
|
||||
let currentChar = "";
|
||||
let currentArr = [];
|
||||
for (const artistName of artists) {
|
||||
char = Array.from(artistName)[0].toUpperCase();
|
||||
if (currentChar == "") {
|
||||
currentChar = char;
|
||||
}
|
||||
if (char == currentChar) {
|
||||
currentArr.push(artistName);
|
||||
} else {
|
||||
charMap.set(currentChar, currentArr);
|
||||
currentChar = char;
|
||||
currentArr = [artistName];
|
||||
}
|
||||
}
|
||||
if (currentChar !== "") {
|
||||
charMap.set(currentChar, currentArr);
|
||||
}
|
||||
console.log(...charMap);
|
||||
});
|
||||
|
||||
Pebble.sendAppMessage({ PKJS_READY: 1 });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function callAPI(method, endpoint) {
|
||||
Pebble.addEventListener("appmessage", function (dict) {
|
||||
if (dict.payload["OUT_MENU_NAV_SCOPE"]) {
|
||||
//const cfg = JSON.parse(localStorage.getItem('clay-settings'));
|
||||
//callAPI(cfg.PKJS_PAVLOK_API_KEY, parseInt(dict.payload["PKJS_PAVLOK_ZAP"]));
|
||||
}
|
||||
});
|
||||
|
||||
function reportAPIFailure() {
|
||||
console.log("API FAILURE");
|
||||
}
|
||||
|
||||
function callAPI(method, endpoint, callback) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onload = function () {
|
||||
switch (xhr.status) {
|
||||
case 200:
|
||||
return this.responseText;
|
||||
callback(this.responseText);
|
||||
return;
|
||||
default:
|
||||
reportAPIFailure(); // TODO
|
||||
reportAPIFailure();
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
reportAPIFailure(); // TODO
|
||||
reportAPIFailure();
|
||||
};
|
||||
xhr.ontimeout = function () {
|
||||
reportAPIFailure(); // TODO
|
||||
reportAPIFailure();
|
||||
};
|
||||
|
||||
xhr.open(method, endpoint, true);
|
||||
xhr.timeout = 8000;
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.setRequestHeader("Authorization", 'MediaBrowser Token="' + apiKey + '"'); // TODO
|
||||
xhr.setRequestHeader("Authorization", 'MediaBrowser Token="' + cfg.PRIV_API_KEY + '"');
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user