Skip to content

Commit

Permalink
- use curl to implement remove track from playlist
Browse files Browse the repository at this point in the history
  (QML does not support a DELETE request with a body
- since Spotify keeps on playing a certain playlist snapshot
  we need to watch for it to end and then trigger a restart to play
  newly added tracks. (trying to mimic a queue)
  • Loading branch information
wdehoog committed Sep 11, 2018
1 parent 9ba000e commit 562cf1a
Show file tree
Hide file tree
Showing 8 changed files with 499 additions and 21 deletions.
6 changes: 4 additions & 2 deletions hutspot.pro
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ SOURCES += \
src/o2/o2spotify.cpp \
src/o2/o2simplecrypt.cpp \
src/spotify.cpp \
src/hutspot.cpp
src/hutspot.cpp \
src/qdeclarativeprocess.cpp

DISTFILES += \
qml/cover/CoverPage.qml \
Expand Down Expand Up @@ -80,6 +81,7 @@ HEADERS += \
src/o2/o2replyserver.h \
src/o2/o2spotify.h \
src/spotify.h \
src/IconProvider.h
src/IconProvider.h \
src/qdeclarativeprocess.h

#QMAKE_LFLAGS += -lavahi-client -lavahi-common
21 changes: 19 additions & 2 deletions qml/Spotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ function reorderTracksInPlaylist(userId, playlistId, rangeStart, insertBefore, o
* @param {function(Object,Object)} callback An optional callback that receives 2 parameters. The first
* one is the error object (null if no error), and the second is the value if the request succeeded.
* @return {Object} Null if a callback is provided, a `Promise` object otherwise
*/
* /
function removeTracksFromPlaylist(userId, playlistId, uris, callback) {
var dataToBeSent = uris.map(function(uri) {
if (typeof uri === 'string') {
Expand All @@ -855,6 +855,23 @@ function removeTracksFromPlaylist(userId, playlistId, uris, callback) {
postData: { tracks: dataToBeSent }
};
return _checkParamsAndPerformRequest(requestData, {}, callback);
};*/
function removeTracksFromPlaylist(playlistId, uris, callback) {
var dataToBeSent = uris.map(function(uri) {
if (typeof uri === 'string') {
return { uri: uri };
} else {
return uri;
}
});

var requestData = {
url: _baseUri + '/playlists/' + playlistId + '/tracks',
type: 'DELETE',
contentType: 'application/json',
postData: { tracks: dataToBeSent }
};
return _checkParamsAndPerformRequest(requestData, {}, callback);
};

/**
Expand Down Expand Up @@ -1485,7 +1502,7 @@ function transferMyPlayback(deviceIds, options, callback) {
function play(options, callback) {
var params = 'device_id' in options ? {device_id: options.device_id} : null;
var postData = {};
['context_uri', 'uris', 'offset'].forEach(function(field) {
['context_uri', 'uris', 'offset', 'position_ms'].forEach(function(field) {
if (field in options) {
postData[field] = options[field];
}
Expand Down
122 changes: 114 additions & 8 deletions qml/hutspot.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Sailfish.Silica 1.0

import org.nemomobile.configuration 1.0
import org.nemomobile.mpris 1.0
import org.hildon.components 1.0

import "Spotify.js" as Spotify
import "Util.js" as Util
Expand Down Expand Up @@ -280,8 +281,14 @@ ApplicationWindow {
})
}

function playContext(context) {
Spotify.play({'device_id': deviceId.value, 'context_uri': context.uri}, function(error, data) {
function playContext(context, options) {
if(options === undefined)
options = {'device_id': deviceId.value, 'context_uri': context.uri}
else {
options.device_id = deviceId.value
options.context_uri = context.uri
}
Spotify.play(options, function(error, data) {
if(!error) {
playing = true
refreshPlayingInfo()
Expand Down Expand Up @@ -671,6 +678,7 @@ ApplicationWindow {
var ev = new Util.PlayListEvent(Util.PlaylistEventType.AddedTrack,
ms.selectedItem.playlist.id, data.snapshot_id)
ev.trackId = track.id
ev.trackUri = track.uri
playlistEvent(ev)
console.log("addToPlaylist: added \"")
} else
Expand All @@ -684,10 +692,18 @@ ApplicationWindow {
function removeFromPlaylist(playlist, track, callback) {
app.showConfirmDialog(qsTr("Please confirm to remove:<br><br><b>" + track.name + "</b>"),
function() {
Spotify.removeTracksFromPlaylist(id, playlist.id, [track.uri], function(error, data) {
// does not work due to Qt. cannot have DELETE request with a body
/*Spotify.removeTracksFromPlaylist(playlist.id, [track.uri], function(error, data) {
callback(error, data)
var ev = new Util.PlayListEvent(Util.PlaylistEventType.RemovedTrack,
ms.selectedItem.playlist.id, data.snapshot_id)
playlist.id, data.snapshot_id)
ev.trackId = track.id
playlistEvent(ev)
})*/
removeTracksFromPlaylistUsingCurl(playlist.id, [track.uri], function(error, data) {
callback(error, data)
var ev = new Util.PlayListEvent(Util.PlaylistEventType.RemovedTrack,
playlist.id, data.snapshot_id)
ev.trackId = track.id
playlistEvent(ev)
})
Expand Down Expand Up @@ -725,6 +741,7 @@ ApplicationWindow {
var ev = new Util.PlayListEvent(Util.PlaylistEventType.ReplacedAllTracks,
playlistId, data.snapshot_id)
playlistEvent(ev)
console.log("replaceTracksInPlaylist: snapshot: " + data.snapshot_id)
} else
console.log("No Data while replacing tracks in Playlist " + playlistId)
})
Expand Down Expand Up @@ -952,10 +969,27 @@ ApplicationWindow {
getHutspotQueuePlaylist(function(success) {
if(!success)
return
if(playingPage.currentId !== hutspotQueuePlaylistId)
// There is a big problem with the Spotify API.
// When adding a track to an already playing playlist it is ignored.
// See https://github.com/spotify/web-api/issues/462
// We can only restart playing but it will affect the current playing track
if(playingPage.currentId !== hutspotQueuePlaylistId) {
playContext({uri: hutspotQueuePlaylistUri})
else if(!playingPage.playbackState.is_playing)
Spotify.play({}, function(error, data) {})
/*} else if(playingPage.currentSnapshotId !== hutspotQueuePlaylistSnapshotId) {
// Due to the above mentioned issue #462 we request to play the same playlist again
// and asking to continue with the current track at the current position.
// This will result in 'skipping' but for now I see no other way
// Mmm. This does not work as well. Maybe Spotify thinks 'hey you are already plying this playlist'.
var currentTrackUri = playingPage.currentTrackUri
var currentTrackPosition = playingPage.playbackState.progress_ms
var options = {}
if(currentTrackUri !== undefined && currentTrackUri.length > 0) {
options.offset = {uri: currentTrackUri}
options.position_ms = currentTrackPosition
}
playContext({uri: hutspotQueuePlaylistUri}, options)*/
} else if(!playingPage.playbackState.is_playing)
pause(function(error, data){})
})
}

Expand All @@ -966,10 +1000,11 @@ ApplicationWindow {
if(data) {
var ev = new Util.PlayListEvent(Util.PlaylistEventType.AddedTrack,
hutspotQueuePlaylistId, data.snapshot_id)
ev.uri = hutspotQueuePlaylistUri
ev.trackId = track.id
playlistEvent(ev)
ensureQueueIsPlaying()
console.log("addToQueue: added " + track.name)
console.log("addToQueue: snapshot: " + data.snapshot_id + "added " + track.name)
} else {
showErrorMessage(undefined, qsTr("Failed to add Track to the Queue"))
console.log("addToPlaylist: failed to add " + track.name)
Expand All @@ -982,6 +1017,19 @@ ApplicationWindow {
})
}

// Debugging
/*Timer {
interval: 5000
running: app.hasValidToken && hutspotQueuePlaylistId.length > 0
repeat: true
onTriggered: {
Spotify.getPlaylist(hutspotQueuePlaylistId, function(error, data) {
if(data)
console.log("Timer.getPlaylist snapshot: " + data.snapshot_id + ", tracks: " + data.tracks.total)
})
}
}*/

function replaceQueueWith(tracks) {
app.getHutspotQueuePlaylist(function(success) {
if(success) {
Expand Down Expand Up @@ -1223,5 +1271,63 @@ ApplicationWindow {
defaultValue: 0
}*/

// QML seems unable to send a http DELETE request with a body.
// Therefore this is done using curl
//
// curl -X DELETE -i -H "Authorization: Bearer {your access token}"
// -H "Content-Type: application/json" "https://api.spotify.com/v1/playlists/71m0QB5fUFrnqfnxVerUup/tracks"
// --data "{\"tracks\":[{\"uri\": \"spotify:track:4iV5W9uYEdYUVa79Axb7Rh\", \"positions\": [2] },{\"uri\":\"spotify:track:1301WleyT98MSxVHPZCA6M\", \"positions\": [7] }] }"

function removeTracksFromPlaylistUsingCurl(playlistId, uris, callback) {
var command = "/usr/bin/curl"
var args = []
args.push("-X")
args.push("DELETE")
//args.push("-i") // include headers in the output
args.push("-H")
args.push("Authorization: Bearer " + Spotify.getAccessToken())
args.push("-H")
args.push("Content-Type: application/json")
args.push(Spotify._baseUri + "/playlists/" + playlistId + "/tracks")
args.push("--data")
args.push("@-")

var data = "{\"tracks\":["
for(var i=0;i<uris.length;i++) {
if(i>0)
data += ","
data += "{\"uri\": \"" + uris[i] + "\"}"
}
data += "]}"

process.callback = callback
process.start(command, args)
process.write(data)
process.closeWriteChannel()
}

Process {
id: process

property var callback: undefined

workingDirectory: "/home/nemo"

onError: {
if(callback !== undefined)
callback(process.error, undefined)
console.log("Process.Error: " + process.error)
callback = undefined
}

onFinished: {
var output = process.readAllStandardOutput()
console.log("Process.Finished: " + process.exitStatus + ", code: " + process.exitCode)
console.log(output)
if(callback !== undefined)
callback(null, JSON.parse(output))
callback = undefined
}
}
}

67 changes: 61 additions & 6 deletions qml/pages/Playing.qml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Page {
property string currentId: ""
property string currentSnapshotId: ""
property string currentTrackId: ""
property string currentTrackUri: ""

property string viewMenuText: ""
property bool showTrackInfo: true
Expand Down Expand Up @@ -613,6 +614,22 @@ Page {
reloadTracks()
}

// try to detect end of playlist play
property bool _isPlaying: false
onPlaybackStateChanged: {
if(playbackState === undefined)
return

if(!_isPlaying && playbackState.is_playing)
console.log("Started Playing")
else if(_isPlaying && !playbackState.is_playing) {
console.log("Stopped Playing")
pluOnStopped()
}

_isPlaying = playbackState.is_playing
}

function refreshPlaybackState() {
var i;

Expand Down Expand Up @@ -645,6 +662,7 @@ Page {
contextObject = data
if(data)
currentSnapshotId = data.snapshot_id
console.log("Now playing snaphot: " + data.snapshot_id)
pageHeaderText = qsTr("Playing Playlist")
})
loadPlaylistTracks(app.id, cid)
Expand Down Expand Up @@ -682,6 +700,7 @@ Page {
playingObject = data
app.newPlayingTrackInfo(data.item)
currentTrackId = playingObject.item.id
currentTrackUri = playingObject.item.uri
}
})

Expand Down Expand Up @@ -810,6 +829,21 @@ Page {
return ct === Spotify.ItemType.Album || ct === Spotify.ItemType.Playlist
}*/

//
// Playlist Utilities
//

property var waitForEndSnapshotData: ({})
property bool waitForEndOfSnapshot : false
function pluOnStopped() {
if(waitForEndOfSnapshot) {
waitForEndOfSnapshot = false
currentId = "" // trigger reload
playContext({uri: waitForEndSnapshotData.uri},
{offset: {uri: waitForEndSnapshotData.trackUri}})
}
}

Connections {
target: app

Expand All @@ -825,22 +859,43 @@ Page {
if(getContextType() !== Spotify.ItemType.Playlist
|| contextObject.id !== event.playlistId)
return

// When a plylist is modified while being played the modifications
// are ignored, it keeps on playing the snapshot that was started.
// To try to fix this we:
// AddedTrack:
// wait for playing to end (last track of original snapshot) and then restart playing
// RemovedTrack:
// for now nothing
// ReplacedAllTracks:
// restart playing

switch(event.type) {
case Util.PlaylistEventType.AddedTrack:
// in theory it has been added at the end of the list
// so we could load the info and add it to the model but ...
loadPlaylistTracks(app.id, currentId)
currentSnapshotId = event.snapshotId
if(_isPlaying) {
waitForEndOfSnapshot = true
waitForEndSnapshotData.uri = event.uri
waitForEndSnapshotData.snapshotId = event.snapshotId
waitForEndSnapshotData.index = contextObject.tracks.total // not used
waitForEndSnapshotData.trackUri = event.trackUri
} else
currentSnapshotId = event.snapshotId
break
case Util.PlaylistEventType.RemovedTrack:
Util.removeFromListModel(searchModel, Spotify.ItemType.Track, event.trackId)
currentSnapshotId = event.snapshotId
//Util.removeFromListModel(searchModel, Spotify.ItemType.Track, event.trackId)
//currentSnapshotId = event.snapshotId
break
case Util.PlaylistEventType.ReplacedAllTracks:
if(currentSnapshotId !== event.snapshotId) {
if(_isPlaying)
app.pause(function(error, data) {
currentId = "" // trigger reload)
playContext({context: contextObject.uri})
})
else
loadPlaylistTracks(app.id, currentId)
currentSnapshotId = event.snapshotId
}
break
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/hutspot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "IconProvider.h"
#include "spotify.h"
//#include "servicebrowser.h"
#include "qdeclarativeprocess.h"

int main(int argc, char *argv[])
{
Expand All @@ -32,6 +33,9 @@ int main(int argc, char *argv[])
Spotify spotify;
view->rootContext()->setContextProperty("spotify", &spotify);

qmlRegisterUncreatableType<QDeclarativeProcessEnums>("org.hildon.components", 1, 0, "Processes", "");
qmlRegisterType<QDeclarativeProcess>("org.hildon.components", 1, 0, "Process");

// custom icon loader
QQmlEngine *engine = view->engine();
engine->addImageProvider(QLatin1String("hutspot-icons"), new IconProvider);
Expand Down
Loading

0 comments on commit 562cf1a

Please sign in to comment.