mirror of
https://github.com/memen45/SubMusic.git
synced 2026-02-18 00:57:39 +01:00
added support for updating play count to server (feedback required)
added warning for possibly hanging sync
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
Version [] -
|
||||
- (tentative) added 'Manage...' option for more playlist options
|
||||
- (tentative) added alpha podcast implementation (feedback needed)
|
||||
- (tentative) added support for updating play count to server (scrobble/record_play)
|
||||
|
||||
Version [0.0.23] - 2020-12-
|
||||
Version [0.0.23] - 2020-12-15
|
||||
- added support for updating play count to server (feedback required)
|
||||
- added warning for possibly hanging sync
|
||||
- update last sync in menu
|
||||
- refactor in menus
|
||||
- fixes potential ampache crash
|
||||
|
||||
@@ -11,7 +11,6 @@ class AmpacheAPI {
|
||||
private var d_client;
|
||||
private var d_hash; // password hash, required for every handshake
|
||||
|
||||
private var d_server; // ping response
|
||||
private var d_session; // handshake response
|
||||
private var d_expire; // Moment of session expire
|
||||
|
||||
@@ -43,34 +42,6 @@ class AmpacheAPI {
|
||||
d_expire = expire;
|
||||
}
|
||||
|
||||
function update(settings) {
|
||||
System.println("AmpacheAPI::update(settings)");
|
||||
|
||||
// update the settings
|
||||
set(settings);
|
||||
|
||||
deleteSession();
|
||||
}
|
||||
|
||||
function deleteSession() {
|
||||
// reset the session
|
||||
d_expire = new Time.Moment(0);
|
||||
Application.Storage.deleteValue("AMPACHE_API_SESSION");
|
||||
d_session = null;
|
||||
}
|
||||
|
||||
function set(settings) {
|
||||
d_url = settings.get("api_url") + "/server/json.server.php";
|
||||
d_usr = settings.get("api_usr");
|
||||
|
||||
// hash the password
|
||||
var hasher = new Cryptography.Hash({:algorithm => Cryptography.HASH_SHA256});
|
||||
hasher.update(string_to_ba(settings.get("api_key")));
|
||||
d_hash = ba_to_hexstring(hasher.digest());
|
||||
|
||||
System.println("AmpacheAPI::set(url: " + d_url + ", user: " + d_usr + ", pass: " + d_hash + ")");
|
||||
}
|
||||
|
||||
/*
|
||||
* API call 'handshake'
|
||||
*/
|
||||
@@ -130,20 +101,29 @@ class AmpacheAPI {
|
||||
// auth is optional
|
||||
// params.put("auth", d_session.get("auth"));
|
||||
|
||||
Communications.makeWebRequest(d_url, params, {}, self.method(:onPing));
|
||||
Communications.makeWebRequest(d_url, params, {}, self.method(:onDictionaryResponse));
|
||||
}
|
||||
|
||||
function onPing(responseCode, data) {
|
||||
System.println("AmpacheAPI::onPing with responseCode " + responseCode + " payload " + data);
|
||||
function record_play(callback, params) {
|
||||
System.println("AmpacheAPI::record_play( params: " + params + ")");
|
||||
|
||||
// errors are filtered first
|
||||
var error = checkDictionaryResponse(responseCode, data);
|
||||
if (error) {
|
||||
d_fallback.invoke(error);
|
||||
return;
|
||||
}
|
||||
d_callback = callback;
|
||||
|
||||
params.put("action", "record_play");
|
||||
params.put("auth", d_session.get("auth"));
|
||||
|
||||
d_callback.invoke(data);
|
||||
Communications.makeWebRequest(d_url, params, {}, self.method(:onDictionaryResponse));
|
||||
}
|
||||
|
||||
function scrobble(callback, params) {
|
||||
System.println("AmpacheAPI::scrobble( params: " + params + ")");
|
||||
|
||||
d_callback = callback;
|
||||
|
||||
params.put("action", "scrobble");
|
||||
params.put("auth", d_session.get("auth"));
|
||||
|
||||
Communications.makeWebRequest(d_url, params, {}, self.method(:onDictionaryResponse));
|
||||
}
|
||||
|
||||
// returns array of playlist objects
|
||||
@@ -151,10 +131,6 @@ class AmpacheAPI {
|
||||
System.println("AmpacheAPI::playlists( params: " + params + ")");
|
||||
|
||||
d_callback = callback;
|
||||
|
||||
if (params == null) {
|
||||
params = {};
|
||||
}
|
||||
|
||||
params.put("action", "playlists");
|
||||
params.put("auth", d_session.get("auth"));
|
||||
@@ -227,6 +203,11 @@ class AmpacheAPI {
|
||||
return now.lessThan(d_expire);
|
||||
}
|
||||
|
||||
/*
|
||||
* onArrayResponse
|
||||
*
|
||||
* default handler for actions that return an array
|
||||
*/
|
||||
function onArrayResponse(responseCode, data) {
|
||||
System.println("AmpacheAPI::onArrayResponse with responseCode: " + responseCode + ", payload " + data);
|
||||
|
||||
@@ -248,6 +229,24 @@ class AmpacheAPI {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* onDictionaryResponse
|
||||
*
|
||||
* Default handler for actions that return a dictionary
|
||||
*/
|
||||
function onDictionaryResponse(responseCode, data) {
|
||||
System.println("AmpacheAPI::onDictionaryResponse with responseCode " + responseCode + " payload " + data);
|
||||
|
||||
// errors are filtered first
|
||||
var error = checkDictionaryResponse(responseCode, data);
|
||||
if (error) {
|
||||
d_fallback.invoke(error);
|
||||
return;
|
||||
}
|
||||
|
||||
d_callback.invoke(data);
|
||||
}
|
||||
|
||||
function checkDictionaryResponse(responseCode, data) {
|
||||
var error = checkResponse(responseCode, data);
|
||||
if (error) { return error; }
|
||||
@@ -257,6 +256,11 @@ class AmpacheAPI {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* checkResponse
|
||||
*
|
||||
* returns response / api errors if found
|
||||
*/
|
||||
function checkResponse(responseCode, data) {
|
||||
var error = SubMusic.HttpError.is(responseCode);
|
||||
if (error) { return error; }
|
||||
@@ -342,4 +346,36 @@ class AmpacheAPI {
|
||||
};
|
||||
return StringUtil.convertEncodedString(ba, options);
|
||||
}
|
||||
|
||||
function update(settings) {
|
||||
System.println("AmpacheAPI::update(settings)");
|
||||
|
||||
// update the settings
|
||||
set(settings);
|
||||
|
||||
deleteSession();
|
||||
}
|
||||
|
||||
function set(settings) {
|
||||
d_url = settings.get("api_url") + "/server/json.server.php";
|
||||
d_usr = settings.get("api_usr");
|
||||
|
||||
// hash the password
|
||||
var hasher = new Cryptography.Hash({:algorithm => Cryptography.HASH_SHA256});
|
||||
hasher.update(string_to_ba(settings.get("api_key")));
|
||||
d_hash = ba_to_hexstring(hasher.digest());
|
||||
|
||||
System.println("AmpacheAPI::set(url: " + d_url + ", user: " + d_usr + ", pass: " + d_hash + ")");
|
||||
}
|
||||
|
||||
function deleteSession() {
|
||||
// reset the session
|
||||
d_expire = new Time.Moment(0);
|
||||
Application.Storage.deleteValue("AMPACHE_API_SESSION");
|
||||
d_session = null;
|
||||
}
|
||||
|
||||
function client() {
|
||||
return d_client;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class AmpacheProvider {
|
||||
|
||||
enum {
|
||||
AMPACHE_ACTION_PING,
|
||||
AMPACHE_ACTION_RECORD_PLAY,
|
||||
AMPACHE_ACTION_PLAYLIST,
|
||||
AMPACHE_ACTION_PLAYLISTS,
|
||||
AMPACHE_ACTION_PLAYLIST_SONGS,
|
||||
@@ -34,6 +35,7 @@ class AmpacheProvider {
|
||||
|
||||
// functions:
|
||||
// - ping returns an object with server version
|
||||
// - recordPlay submit a play
|
||||
// - getAllPlaylists returns array of all playlists available for Ampache user
|
||||
// - getPlaylist returns an array of one playlist object with id
|
||||
// - getPlaylistSongs returns an array of songs on the playlist with id
|
||||
@@ -54,6 +56,19 @@ class AmpacheProvider {
|
||||
do_();
|
||||
}
|
||||
|
||||
function recordPlay(id, time, callback) {
|
||||
d_callback = callback;
|
||||
|
||||
d_params = {
|
||||
"id" => id,
|
||||
"client" => d_api.client(),
|
||||
"date" => time,
|
||||
};
|
||||
|
||||
d_action = AMPACHE_ACTION_RECORD_PLAY;
|
||||
do_();
|
||||
}
|
||||
|
||||
/**
|
||||
* getAllPlaylists
|
||||
*
|
||||
@@ -144,6 +159,13 @@ class AmpacheProvider {
|
||||
d_callback.invoke(response);
|
||||
}
|
||||
|
||||
function on_do_record_play(response) {
|
||||
System.println("AmpacheProvider::on_do_record_play( response = " + response + ")");
|
||||
|
||||
d_action = null;
|
||||
d_callback.invoke(response["success"]); // expected success string
|
||||
}
|
||||
|
||||
function on_do_playlist(response) {
|
||||
// append the standard playlist objects to the array
|
||||
for (var idx = 0; idx < response.size(); ++idx) {
|
||||
@@ -239,6 +261,10 @@ class AmpacheProvider {
|
||||
d_api.handshake(self.method(:do_));
|
||||
return;
|
||||
}
|
||||
if (d_action == AMPACHE_ACTION_RECORD_PLAY) {
|
||||
d_api.record_play(self.method(:on_do_record_play), d_params);
|
||||
return;
|
||||
}
|
||||
if (d_action == AMPACHE_ACTION_PLAYLIST) {
|
||||
d_api.playlist(self.method(:on_do_playlist), d_params);
|
||||
return;
|
||||
|
||||
@@ -10,7 +10,7 @@ class ArrayStore extends Store {
|
||||
|
||||
// returns a connected item
|
||||
function get(idx) {
|
||||
System.println("Store::get( idx : " + idx + " )");
|
||||
// System.println("ArrayStore::get( idx : " + idx + " )");
|
||||
|
||||
var items = Store.value();
|
||||
if ((idx == null)
|
||||
@@ -31,7 +31,7 @@ class ArrayStore extends Store {
|
||||
}
|
||||
|
||||
function add(item) {
|
||||
System.println("Store::save( item : " + item.toStorage() + " )");
|
||||
System.println("ArrayStore::add( item : " + item.toStorage() + " )");
|
||||
|
||||
// return false if failed save
|
||||
if (item == null) {
|
||||
@@ -45,12 +45,19 @@ class ArrayStore extends Store {
|
||||
|
||||
// returns true if item id entry removed from storage or is not in storage
|
||||
function remove(item) {
|
||||
System.println("Store::remove( item : " + item + " )");
|
||||
System.println("ArrayStore::remove( item : " + item + " )");
|
||||
if (item == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Store.value().remove(item);
|
||||
Store.value().remove(item.toStorage());
|
||||
return Store.update();
|
||||
}
|
||||
|
||||
function removeAll() {
|
||||
System.println("ArrayStore::removeAll()");
|
||||
|
||||
setValue([]);
|
||||
return Store.update();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class ObjectStore extends Store {
|
||||
|
||||
// returns a connected item
|
||||
function get(id) {
|
||||
System.println("ObjectStore::get( id : " + id + " )");
|
||||
// System.println("ObjectStore::get( id : " + id + " )");
|
||||
|
||||
var items = Store.value();
|
||||
if (id == null) {
|
||||
@@ -21,7 +21,7 @@ class ObjectStore extends Store {
|
||||
}
|
||||
|
||||
function getIds() {
|
||||
System.println("ObjectStore::getIds()");
|
||||
// System.println("ObjectStore::getIds()");
|
||||
|
||||
return Store.value().keys();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using Toybox.System;
|
||||
using Toybox.Application;
|
||||
|
||||
module PlaylistStore {
|
||||
var d_store = new ObjectStore(Storage.PLAYLISTS);
|
||||
|
||||
|
||||
@@ -141,7 +141,8 @@ class PlaylistSync extends Deferrable {
|
||||
}
|
||||
|
||||
// update playlist info if not found
|
||||
if (error.type() == SubMusic.ApiError.NOTFOUND) {
|
||||
if ((error instanceof SubMusic.ApiError)
|
||||
&& (error.type() == SubMusic.ApiError.NOTFOUND)) {
|
||||
d_playlist.setRemote(false);
|
||||
}
|
||||
|
||||
|
||||
54
source/ScrobbleStore.mc
Normal file
54
source/ScrobbleStore.mc
Normal file
@@ -0,0 +1,54 @@
|
||||
class Scrobble {
|
||||
|
||||
private var d_id;
|
||||
private var d_time;
|
||||
|
||||
function initialize(storage) {
|
||||
d_id = storage["id"];
|
||||
|
||||
d_time = storage["time"];
|
||||
if (d_time == null) {
|
||||
d_time = Time.now().value().format("%i");
|
||||
}
|
||||
}
|
||||
|
||||
function id() {
|
||||
return d_id;
|
||||
}
|
||||
|
||||
function time() {
|
||||
return d_time;
|
||||
}
|
||||
|
||||
function toStorage() {
|
||||
return {
|
||||
"id" => d_id,
|
||||
"time" => d_time,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module ScrobbleStore {
|
||||
|
||||
var d_store = new ArrayStore(Storage.PLAY_RECORDS);
|
||||
|
||||
function add(item) {
|
||||
d_store.add(item);
|
||||
}
|
||||
|
||||
function get(idx) {
|
||||
return d_store.get(idx);
|
||||
}
|
||||
|
||||
function size() {
|
||||
return d_store.size();
|
||||
}
|
||||
|
||||
function remove(item) {
|
||||
return d_store.remove(item);
|
||||
}
|
||||
|
||||
function removeAll() {
|
||||
return d_store.removeAll();
|
||||
}
|
||||
}
|
||||
91
source/ScrobbleSync.mc
Normal file
91
source/ScrobbleSync.mc
Normal file
@@ -0,0 +1,91 @@
|
||||
class ScrobbleSync extends Deferrable {
|
||||
|
||||
private var d_provider;
|
||||
|
||||
private var f_progress;
|
||||
|
||||
private var d_scrobble = null;
|
||||
private var d_idx = 0;
|
||||
private var d_todo = ScrobbleStore.size();
|
||||
private var d_todo_total = ScrobbleStore.size();
|
||||
|
||||
function initialize(provider, progress) {
|
||||
Deferrable.initialize(method(:sync)); // make sync the deferred task
|
||||
|
||||
f_progress = progress;
|
||||
|
||||
d_provider = provider;
|
||||
d_provider.setFallback(method(:onError));
|
||||
}
|
||||
|
||||
function sync() {
|
||||
if (d_todo == 0) {
|
||||
return Deferrable.complete();
|
||||
}
|
||||
|
||||
// update progress
|
||||
f_progress.invoke(progress());
|
||||
|
||||
// record first play on the list
|
||||
var storage = ScrobbleStore.get(d_idx);
|
||||
d_scrobble = new Scrobble(storage);
|
||||
d_provider.recordPlay(d_scrobble.id(), d_scrobble.time(), method(:onRecordPlay));
|
||||
return Deferrable.defer();
|
||||
}
|
||||
|
||||
function onRecordPlay(response) {
|
||||
System.println("ScrobbleSync::onRecordPlay( response : " + response + ")");
|
||||
|
||||
// decrement todos
|
||||
d_todo -= 1;
|
||||
|
||||
// remove front as it is done now
|
||||
ScrobbleStore.remove(d_scrobble);
|
||||
|
||||
// sync next
|
||||
sync();
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
System.println("ScrobbleSync::onError( " + error.shortString() + " " + error.toString() + ")");
|
||||
|
||||
// some problem with the network - we can skip this and try again later
|
||||
if ((error instanceof SubMusic.HttpError)
|
||||
|| (error instanceof SubMusic.GarminSdkError)) {
|
||||
// decrement todos
|
||||
d_todo -= 1;
|
||||
|
||||
// increment idx as first is stored, but not reported yet
|
||||
d_idx += 1;
|
||||
|
||||
// sync next
|
||||
sync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nextcloud has 'missing method' and reports
|
||||
//
|
||||
// if ((error instanceof AmpacheError)
|
||||
// && (error.code() == AmpacheError.METHOD_MISSING))
|
||||
//
|
||||
// if ((error instanceof SubsonicError)
|
||||
// && (error.code() == SubsonicError.NOT_FOUND))
|
||||
//
|
||||
// perform default action
|
||||
|
||||
// default action is to clear the list
|
||||
|
||||
// remove all records, since it is not possible to store them anyways
|
||||
ScrobbleStore.removeAll();
|
||||
|
||||
// mark complete
|
||||
Deferrable.complete();
|
||||
}
|
||||
|
||||
function progress() {
|
||||
var done = d_todo_total - d_todo;
|
||||
var progress = (100 * done) / d_todo_total.toFloat();
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ class Store {
|
||||
private var d_value = null; // variable that is reflected in the Application.Storage
|
||||
|
||||
function initialize(key, value) {
|
||||
System.println("Store::initialize()");
|
||||
|
||||
d_key = key;
|
||||
|
||||
var stored = Application.Storage.getValue(d_key);
|
||||
@@ -22,9 +20,18 @@ class Store {
|
||||
function value() {
|
||||
return d_value;
|
||||
}
|
||||
|
||||
function setValue(value) {
|
||||
d_value = value;
|
||||
}
|
||||
|
||||
function update() {
|
||||
Application.Storage.setValue(d_key, d_value);
|
||||
return true; // true if successful
|
||||
var success = true;
|
||||
try {
|
||||
Application.Storage.setValue(d_key, d_value);
|
||||
} catch (exception instanceof Toybox.Lang.StorageFullException) {
|
||||
success = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using Toybox.Media;
|
||||
class SubMusicContentDelegate extends Media.ContentDelegate {
|
||||
|
||||
private var d_iterator;
|
||||
|
||||
enum { START, SKIP_NEXT, SKIP_PREVIOUS, PLAYBACK_NOTIFY, COMPLETE, STOP, PAUSE, RESUME, }
|
||||
private var d_events = ["Start", "Skip Next", "Skip Previous", "Playback Notify", "Complete", "Stop", "Pause", "Resume"];
|
||||
|
||||
function initialize() {
|
||||
@@ -57,6 +59,7 @@ class SubMusicContentDelegate extends Media.ContentDelegate {
|
||||
function onMore() {
|
||||
System.println("onMore is called");
|
||||
}
|
||||
|
||||
function onLibrary() {
|
||||
System.println("onLibrary is called");
|
||||
}
|
||||
@@ -65,8 +68,28 @@ class SubMusicContentDelegate extends Media.ContentDelegate {
|
||||
// been triggered for the given song
|
||||
function onSong(contentRefId, songEvent, playbackPosition) {
|
||||
System.println("onSong Event (" + d_events[songEvent] + "): " + getSongName(contentRefId) + " at position " + playbackPosition);
|
||||
|
||||
if (songEvent == PLAYBACK_NOTIFY) {
|
||||
var id = findIdByRefId(contentRefId);
|
||||
if (id == null) { return; }
|
||||
ScrobbleStore.add(new Scrobble({
|
||||
"id" => id,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function findIdByRefId(refId) {
|
||||
var ids = SongStore.getIds();
|
||||
for (var idx = 0; idx < ids.size(); ++idx) {
|
||||
var isong = new ISong(ids[idx]);
|
||||
if (refId == isong.refId()) {
|
||||
return ids[idx];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSongName(refId) {
|
||||
return Media.getCachedContentObj(new Media.ContentRef(refId, Media.CONTENT_TYPE_AUDIO)).getMetadata().title;
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ class SubMusicContentIterator extends Media.ContentIterator {
|
||||
PLAYBACK_CONTROL_SKIP_FORWARD,
|
||||
PLAYBACK_CONTROL_SKIP_BACKWARD,
|
||||
];
|
||||
profile.playbackNotificationThreshold = 1;
|
||||
profile.requirePlaybackNotification = false;
|
||||
profile.playbackNotificationThreshold = 30;
|
||||
profile.requirePlaybackNotification = true; // notify played
|
||||
profile.skipPreviousThreshold = 5;
|
||||
profile.supportsPlaylistPreview = true;
|
||||
return profile;
|
||||
@@ -167,7 +167,12 @@ class SubMusicContentIterator extends Media.ContentIterator {
|
||||
|
||||
// Retrieve the cached object from Media
|
||||
function getObj(idx) {
|
||||
return Media.getCachedContentObj(new Media.ContentRef(d_songs[idx], Media.CONTENT_TYPE_AUDIO));
|
||||
var contentRef = new Media.ContentRef(d_songs[idx], Media.CONTENT_TYPE_AUDIO);
|
||||
var content = Media.getCachedContentObj(contentRef);
|
||||
|
||||
var metadata = content.getMetadata();
|
||||
var playbackStartPos = 0; // current playback position is 0, TODO load from storage if podcast
|
||||
return new Media.ActiveContent(contentRef, metadata, playbackStartPos);
|
||||
}
|
||||
|
||||
// reorder the playlist randomly
|
||||
|
||||
@@ -21,10 +21,12 @@ module Storage {
|
||||
|
||||
SONGS, // dictionary where song id is key
|
||||
SONGS_DELETE, // array of song ids of todelete songs (refCount == 0)
|
||||
|
||||
PLAYLISTS, // dictionary where playlist id is key
|
||||
|
||||
LAST_SYNC, // dictionary with details on last sync
|
||||
|
||||
PLAY_RECORDS, // array with play_record objects
|
||||
|
||||
VERSION = 200, // version string of store
|
||||
}
|
||||
|
||||
|
||||
@@ -26,16 +26,25 @@ class SubMusicSyncDelegate extends Communications.SyncDelegate {
|
||||
// show progress
|
||||
Communications.notifySyncProgress(0);
|
||||
|
||||
// starting sync
|
||||
// first sync is on Scrobbles
|
||||
startScrobbleSync();
|
||||
}
|
||||
|
||||
function startPlaylistSync() {
|
||||
// starting sync
|
||||
d_todo = PlaylistStore.getIds();
|
||||
d_todo_total = d_todo.size();
|
||||
|
||||
// start async loop, provide callback to onLoopCompleted
|
||||
d_loop = new DeferredFor(0, d_todo.size(), self.method(:step), self.method(:onComplete));
|
||||
d_loop = new DeferredFor(0, d_todo.size(), self.method(:stepPlaylist), self.method(:onPlaylistsDone));
|
||||
d_loop.run();
|
||||
}
|
||||
|
||||
function onComplete() {
|
||||
function stepPlaylist(idx) {
|
||||
return new PlaylistSync(d_provider, d_todo[idx], method(:onPlaylistProgress));
|
||||
}
|
||||
|
||||
function onPlaylistsDone() {
|
||||
// finalize removals (deletes are deferred, to prevent redownloading)
|
||||
var todelete = SongStore.getDeletes();
|
||||
for (var idx = 0; idx < todelete.size(); ++idx) {
|
||||
@@ -51,20 +60,31 @@ class SubMusicSyncDelegate extends Communications.SyncDelegate {
|
||||
Communications.notifySyncComplete(null);
|
||||
Application.Storage.setValue(Storage.LAST_SYNC, { "time" => Time.now().value(), });
|
||||
}
|
||||
|
||||
function startScrobbleSync() {
|
||||
var deferrable = new ScrobbleSync(d_provider, method(:onScrobbleProgress));
|
||||
deferrable.setCallback(method(:startPlaylistSync));
|
||||
if (deferrable.run()) {
|
||||
startPlaylistSync(); // continue with playlist sync afterwards
|
||||
}
|
||||
// not completed, so wait for callback
|
||||
}
|
||||
|
||||
function onProgress(progress) {
|
||||
function onPlaylistProgress(progress) {
|
||||
System.println("Sync Progress: list " + (d_loop.idx() + 1) + " of " + d_loop.end() + " is on " + progress + " %");
|
||||
|
||||
progress += (100 * d_loop.idx());
|
||||
progress += (100 * d_loop.idx()); // half of 100% for playlist progress
|
||||
progress /= d_loop.end().toFloat();
|
||||
|
||||
System.println(progress.toNumber());
|
||||
Communications.notifySyncProgress(progress.toNumber());
|
||||
}
|
||||
|
||||
function step(idx) {
|
||||
return new PlaylistSync(d_provider, d_todo[idx], method(:onProgress));
|
||||
}
|
||||
|
||||
function onScrobbleProgress(progress) {
|
||||
System.println("Sync Progress: scrobble is on " + progress + " %");
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Sync always needed to verify new songs on the server
|
||||
function isSyncNeeded() {
|
||||
|
||||
@@ -7,6 +7,7 @@ using SubMusic;
|
||||
class SubsonicAPI {
|
||||
|
||||
private var d_base_url;
|
||||
private var d_client;
|
||||
|
||||
private var d_params = {};
|
||||
|
||||
@@ -16,8 +17,8 @@ class SubsonicAPI {
|
||||
function initialize(settings, fallback) {
|
||||
set(settings);
|
||||
|
||||
var client = (WatchUi.loadResource(Rez.Strings.AppName) + " v" + (new SubMusicVersion(null).toString()));
|
||||
d_params.put("c", client);
|
||||
d_client = (WatchUi.loadResource(Rez.Strings.AppName) + " v" + (new SubMusicVersion(null).toString()));
|
||||
d_params.put("c", d_client);
|
||||
d_params.put("v", "1.10.2"); // subsonic api version
|
||||
d_params.put("f", "json"); // request format
|
||||
|
||||
@@ -44,7 +45,35 @@ class SubsonicAPI {
|
||||
}
|
||||
d_callback.invoke(data["subsonic-response"]);
|
||||
}
|
||||
|
||||
function scrobble(callback, params) {
|
||||
System.println("SubsonicAPI::scrobble(params: " + params + ")");
|
||||
|
||||
d_callback = callback;
|
||||
|
||||
var url = d_base_url + "scrobble";
|
||||
|
||||
// construct parameters
|
||||
var id = params["id"];
|
||||
var time = params["time"];
|
||||
params = d_params;
|
||||
params["id"] = id; // set id for scrobble
|
||||
params["time"] = time; // set time for scrobble
|
||||
|
||||
Communications.makeWebRequest(url, params, {}, self.method(:onGetPlaylist));
|
||||
}
|
||||
|
||||
function onScrobble(responseCode, data) {
|
||||
System.println("SubsonicAPI::onScrobble( responseCode: " + responseCode + ", data: " + data + ")");
|
||||
|
||||
// check if request was successful and response is ok
|
||||
var error = checkResponse(responseCode, data);
|
||||
if (error) {
|
||||
d_fallback.invoke(error); // add function name and variables available ?
|
||||
return;
|
||||
}
|
||||
d_callback.invoke(data["subsonic-response"]); // empty response on success
|
||||
}
|
||||
|
||||
/**
|
||||
* getPlaylists
|
||||
@@ -159,8 +188,10 @@ class SubsonicAPI {
|
||||
function update(settings) {
|
||||
System.println("SubsonicAPI::update(settings)");
|
||||
|
||||
// no persistent session info, so only update variables for future requests
|
||||
// update the settings
|
||||
set(settings);
|
||||
|
||||
// no persistent session info, so only update variables for future requests
|
||||
}
|
||||
|
||||
function set(settings) {
|
||||
@@ -170,4 +201,8 @@ class SubsonicAPI {
|
||||
|
||||
System.println("SubsonicAPI::set(url: " + d_base_url + " user: " + d_params.get("u") + ", pass: " + d_params.get("p") + ")");
|
||||
}
|
||||
|
||||
function client() {
|
||||
return d_client;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ class SubsonicError extends SubMusic.ApiError {
|
||||
return d_msg;
|
||||
}
|
||||
|
||||
function code() {
|
||||
return d_code;
|
||||
}
|
||||
|
||||
static function is(responseCode, data) {
|
||||
|
||||
// subsonic API errors have http code 200, status failed and an element 'error'
|
||||
|
||||
@@ -17,6 +17,7 @@ class SubsonicProvider {
|
||||
|
||||
// functions:
|
||||
// - ping returns an object with server version
|
||||
// - recordPlay submit a play
|
||||
// - getAllPlaylists returns array of all playlists available for Subsonic user
|
||||
// - getPlaylistSongs returns an array of songs on the playlist with id
|
||||
// - getRefId returns a refId for a song by id (this downloads the song)
|
||||
@@ -34,6 +35,16 @@ class SubsonicProvider {
|
||||
|
||||
d_api.ping(self.method(:onPing));
|
||||
}
|
||||
|
||||
function recordPlay(id, time, callback) {
|
||||
d_callback = callback;
|
||||
|
||||
var params = {
|
||||
"id" => id,
|
||||
"time" => time,
|
||||
};
|
||||
d_api.scrobble(self.method(:onRecordPlay), params); // scrobble is only way to submit a play
|
||||
}
|
||||
|
||||
/**
|
||||
* getAllPlaylists
|
||||
@@ -104,6 +115,12 @@ class SubsonicProvider {
|
||||
|
||||
d_callback.invoke(response);
|
||||
}
|
||||
|
||||
function onRecordPlay(response) {
|
||||
System.println("SubsonicProvider::onRecordPlay( response = " + response + ")");
|
||||
|
||||
d_callback.invoke(response); // expected empty element
|
||||
}
|
||||
|
||||
function onGetAllPlaylists(response) {
|
||||
System.println("SubsonicProvider::onGetAllPlaylists( response = " + response + ")");
|
||||
|
||||
@@ -79,6 +79,7 @@ module SubMusic {
|
||||
|
||||
function onBack() {
|
||||
if (d_callback) { d_callback.invoke(); }
|
||||
else { Menu2InputDelegate.onBack(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ module SubMusic {
|
||||
}
|
||||
|
||||
function onMoreInfo() {
|
||||
WatchUi.pushView(new SubMusic.Menu.MoreView(), new SubMusic.Menu.Delegate(), WatchUi.SLIDE_IMMEDIATE);
|
||||
WatchUi.pushView(new SubMusic.Menu.MoreView(), new SubMusic.Menu.Delegate(null), WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,4 @@ class TapDelegate extends WatchUi.BehaviorDelegate {
|
||||
d_callback.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
WatchUi.popView(WatchUi.SLIDE_IMMEDIATE);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user