diff --git a/README.md b/README.md index 612ff88..59d8cf2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ following a few simple steps: shpotify needs to connect to Spotify’s API in order to find music by name. It is very likely you want this feature! +Spotify API has a 'public' part, for which developer/app are sufficient, +and it has a 'private' part, for which 'user authorization' is required. + To get this to work, you first need to sign up (or into) Spotify’s developer site and [create an *Application*][spotify-dev]. Once you’ve done so, you can find its `Client ID` and `Client Secret` values and @@ -57,6 +60,16 @@ CLIENT_ID="abc01de2fghijk345lmnop" CLIENT_SECRET="qr6stu789vwxyz" ```` +If 'user authentication' is required, a spotify website will open in your +default browser asking for permission. After granting permission, a new +'localhost' site will be opened on port 8082 with a code in the URL. This code +is caught by Shpotify and is used to get the user-access code and user-refresh +token. From now it is possible to get information linked to your account, like +your playlists, history, devices, etc. + +_note: thise page is supposed to automatically close, but that doesn't always +work properly._ + ## Usage With shpotify you can control Spotify with the following commands: @@ -85,6 +98,7 @@ spotify status Shows the play status, including the current spotify status artist Shows the currently playing artist. spotify status album Shows the currently playing album. spotify status track Shows the currently playing track. +spotify status liked * Shows if a track or album is liked or not. spotify share Displays the current song's Spotify URL and URI. spotify share url Displays the current song's Spotify URL and copies it to the clipboard. @@ -92,6 +106,18 @@ spotify share uri Displays the current song's Spotify URI and c spotify toggle shuffle Toggles shuffle playback mode. spotify toggle repeat Toggles repeat playback mode. + +spotify list uri List information about track, album or playlist by uri. Format in csv|tsv|text|html" +spotify list myalbums * List 50 of your last liked albums, format in csv|tsv|text|html"; +spotify list mytracks * List 50 of your last liked songs, format in csv|tsv|text|html"; +spotify list mine * List 'my' playlists (uri, title, public), format in csv|tsv|text|html"; +spotify list history * List 30 last played tracks (uri, title, artist, album), format in csv|tsv|text|html + +spotify like * Like a song or album and add it to your library."; + +spotify -v | --version Shows the shpotify and spotify versions."; + +* Please note that this requires authentication via a browser." ```` ## Authors and contributing diff --git a/spotify b/spotify index febd502..fc2f2f6 100755 --- a/spotify +++ b/spotify @@ -26,6 +26,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +SHPOTIFY_VERSION="2.1.1_DM_fork" USER_CONFIG_DEFAULTS="CLIENT_ID=\"\"\nCLIENT_SECRET=\"\""; USER_CONFIG_FILE=${SHPOTIFY_CONFIG_FILE:-${HOME}/.shpotify.cfg} if ! [[ -f "${USER_CONFIG_FILE}" ]]; then @@ -34,6 +35,9 @@ if ! [[ -f "${USER_CONFIG_FILE}" ]]; then fi source "${USER_CONFIG_FILE}"; +SHPOTIFY_CREDENTIALS=$(printf "${CLIENT_ID}:${CLIENT_SECRET}" | base64 | tr -d "\n"|tr -d '\r'); + + # Set the percent change in volume for vol up and vol down VOL_INCREMENT=10 @@ -87,6 +91,7 @@ showHelp () { echo " status artist # Shows the currently playing artist."; echo " status album # Shows the currently playing album."; echo " status track # Shows the currently playing track."; + echo " status liked * # Shows if a track or album is liked or not."; echo; echo " share # Displays the current song's Spotify URL and URI." echo " share url # Displays the current song's Spotify URL and copies it to the clipboard." @@ -94,9 +99,30 @@ showHelp () { echo; echo " toggle shuffle # Toggles shuffle playback mode."; echo " toggle repeat # Toggles repeat playback mode."; + if [ $jq_installed = "true" ];then + echo; + echo " list uri # List information about track, album or playlist by uri. Format in csv|tsv|text|html"; + echo " list myalbums * # List 50 of your last liked albums, format in csv|tsv|text|html"; + echo " list mytracks * # List 50 of your last liked songs, format in csv|tsv|text|html"; + echo " list mine * # List 'my' playlists (uri, title, public), format in csv|tsv|texst|html"; + echo " list history * # List 30 last played tracks (uri, title, artist, album), format in csv|tsv|text|html"; + echo " list devices * # List devices to play Spotify on. Devices must be active to be seen."; + echo; + echo " like * # Like a song or album and add it to your library."; + echo; + echo " connect # Connect and play music on another device. ID can be found with 'list devices'."; + fi + echo; + echo " -v | --version # Shows the shpotify and spotify versions."; + echo "* Please note that this requires authentication via a browser."; showAPIHelp } +showVersion() { + cecho "shpotify.sh: $SHPOTIFY_VERSION"; + cecho "spotify: `osascript -e 'get version of application "Spotify"'`" +} + cecho(){ bold=$(tput bold); green=$(tput setaf 2); @@ -116,6 +142,137 @@ showTrack() { echo `osascript -e 'tell application "Spotify" to name of current track as string'`; } +showMyPlaylists() { + MyPlaylists=$( \ + curl -s -G "https://api.spotify.com/v1/me/playlists?limit=20" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + | jq -r --arg format "$format" '.items[] | [ .uri, .name, .public] | @'$format'' \ + ) + cecho "$MyPlaylists" +} + +showMyHistory() { + MyHistory=$( \ + curl -s -G "https://api.spotify.com/v1/me/player/recently-played?limit=30" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + | jq -r --arg format "$format" '.items[] | [ .track.uri, .track.name, .track.artists[0].name, .track.album.name] | @'$format'' \ + ) + cecho "$MyHistory" +} + +showMyDevices() { + MyDevices=$( \ + curl -s -G "https://api.spotify.com/v1/me/player/devices" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + | jq -r --arg format "$format" '.devices[] | [ .id, .name, .type, .is_active, .volume_percent ] | @'$format'' \ + ) + cecho "$MyDevices" +} + +showMyLiked() { + MyLiked=$( \ + curl -s -G "https://api.spotify.com/v1/me/${likefilter}?limit=50" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + ) + + if [[ $likefilter == "tracks" ]];then + MyLiked=$(echo "$MyLiked" | jq -r --arg format "$format" '.items[] | [ .track.uri, .track.name, .track.artists[0].name, .track.album.name] | @'$format'' ) + else + MyLiked=$(echo "$MyLiked" | jq -r --arg format "$format" '.items[] | [ .album.uri, .album.artists[0].name, .album.name, .album.label, .album.release_date, .album.total_tracks, .album.popularity] | @'$format'' ) + fi + cecho "$MyLiked" +} + +checkLiked() { + read URI_TYPE URI_ID <<<$(echo "$URI" | awk -F ":" '{print $2,$3}') + ItIsLiked=$( + curl -s -G "https://api.spotify.com/v1/me/${URI_TYPE}s/contains?ids=${URI_ID}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + ) +} + +showURIinfo() { + read URI_TYPE URI_ID <<<$(echo "$URI" | awk -F ":" '{print $2,$3}') + if [[ $URI_TYPE == "playlist" ]];then + SPOTIFY_TOKEN_URI="https://accounts.spotify.com/api/token"; + getAccessToken + URI_INFO=$( \ + curl -s -G "https://api.spotify.com/v1/${URI_TYPE}s/${URI_ID}/tracks" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_ACCESS_TOKEN} " + ) + else + getUserAccessToken + URI_INFO=$( \ + curl -s -G "https://api.spotify.com/v1/${URI_TYPE}s/${URI_ID}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" \ + ) + fi + +# format=csv + if [[ $URI_TYPE == "playlist" ]];then + URI_INFO=$(echo "$URI_INFO" | jq -r --arg format "$format" '.items[] | [ .track.uri, .track.name, .track.artists[0].name, .track.album.name] | @'$format'' ) + elif [[ $URI_TYPE == "track" ]];then + URI_INFO=$(echo "$URI_INFO" | jq -r --arg format "$format" '[.uri, .name, .artists[0].name, .album.name] | @'$format'' ) + else + URI_INFO=$(echo "$URI_INFO" | jq -r --arg format "$format" '[.uri, .name, .artists[0].name, .release_date] | @'$format'' ) + fi + + cecho "${URI_INFO}" +} + +likeURI() { + + read URI_TYPE URI_ID <<<$(echo "$URI" | awk -F ":" '{print $2,$3}') + if [[ $URI_TYPE == "track" || $URI_TYPE == "album" ]];then + checkLiked + if [[ $ItIsLiked == "[ true ]" ]];then + cecho "oops, you already like `showURIinfo`" + else + curl -X "PUT" "https://api.spotify.com/v1/me/${URI_TYPE}s?ids=${URI_ID}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" + showURIinfo + fi + else + cecho "oops, you try to like a ${URI_TYPE}, but that's not possible for now." + fi +} + +queueTrack() { + curl -X "POST" "https://api.spotify.com/v1/me/player/queue?uri=${URI}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" && \ + cecho "Queued `showURIinfo`" +} + + +selectDevice() { + curl -X "PUT" "https://api.spotify.com/v1/me/player" \ + --data "{\"device_ids\":[\"${device_id}\"]}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${SPOTIFY_USER_ACCESS_TOKEN}" + + sleep 2 + osascript -e 'tell application "Spotify" to play'; +} + showStatus () { state=`osascript -e 'tell application "Spotify" to player state as string'`; cecho "Spotify is currently $state."; @@ -145,6 +302,86 @@ showStatus () { echo -e $reset"Artist: $(showArtist)\nAlbum: $(showAlbum)\nTrack: $(showTrack) \nPosition: $position / $duration"; } +getUserAccessToken() { + ## Based on a gist by Hugh Rawlinson (https://gist.github.com/hughrawlinson/358afa57a04c8c0f1ce4f1fd86604a73) + port=8082 + redirect_uri=http%3A%2F%2Flocalhost%3A$port%2F + auth_endpoint=https://accounts.spotify.com/authorize/?response_type=code\&client_id=$CLIENT_ID\&redirect_uri=$redirect_uri + # TODO return cached access token if scopes match and it hasn't expired + # TODO get scopes from args + scopes="playlist-read-private user-read-playback-state user-modify-playback-state user-read-recently-played user-library-read user-library-modify" + if [[ ! -z $scopes ]] + then + encoded_scopes=$(echo $scopes| tr ' ' '%' | sed s/%/%20/g) + # If scopes exists, then append them to auth_endpoint + auth_endpoint=$auth_endpoint\&scope=$encoded_scopes + fi + + # If refresh_token exists and is valid for scopes, use refresh flow. No new login required + if [[ -r "/var/tmp/shpotify_refresh_token" ]];then + refresh_token=$(cat "/var/tmp/shpotify_refresh_token"); + response=$(curl -s https://accounts.spotify.com/api/token \ + -H "Content-Type:application/x-www-form-urlencoded" \ + -H "Authorization: Basic $(echo -n $CLIENT_ID:$CLIENT_SECRET | base64)" \ + -d "grant_type=refresh_token" \ + -d "refresh_token=$refresh_token" \ + ) + else + open $auth_endpoint + # User is now authenticating on accounts.spotify.com... + + # Now the user gets redirected to our endpoint + # Grab token and close browser window + ### DM: closinig of window/tab doesn't work that well... + response=$(echo "HTTP/1.1 200 OK\nAccess-Control-Allow-Origin:*\nContent-Length:65\n\n\n" | nc -l -c $port) + code=$(echo "$response" | grep GET | cut -d' ' -f 2 | cut -d'=' -f 2) + + response=$(curl -s https://accounts.spotify.com/api/token \ + -H "Content-Type:application/x-www-form-urlencoded" \ + -H "Authorization: Basic ${SHPOTIFY_CREDENTIALS}" \ + -d "grant_type=authorization_code&code=$code&redirect_uri=http%3A%2F%2Flocalhost%3A$port%2F") + + ## refresh token only given during original authentication. + # Store the refrehs_token for later use + echo $response | jq -r '.refresh_token' > /var/tmp/shpotify_refresh_token + fi + + # Output cache access token + SPOTIFY_USER_ACCESS_TOKEN=$(echo $response | jq -r '.access_token') +} + +getAccessToken() { + cecho "Connecting to Spotify's API"; + + SPOTIFY_TOKEN_RESPONSE_DATA=$( \ + curl "${SPOTIFY_TOKEN_URI}" \ + --silent \ + -X "POST" \ + -H "Authorization: Basic ${SHPOTIFY_CREDENTIALS}" \ + -d "grant_type=client_credentials" \ + ) + if ! [[ "${SPOTIFY_TOKEN_RESPONSE_DATA}" =~ "access_token" ]]; then + cecho "Autorization failed, please check ${USER_CONFG_FILE}" + cecho "${SPOTIFY_TOKEN_RESPONSE_DATA}" + showAPIHelp + exit 1 + fi + SPOTIFY_ACCESS_TOKEN=$( \ + printf "${SPOTIFY_TOKEN_RESPONSE_DATA}" \ + | command grep -E -o '"access_token":".*",' \ + | sed 's/"access_token"://g' \ + | sed 's/"//g' \ + | sed 's/,.*//g' \ + ) +} + +## Check existence of jq +if [ -x "$(command -v jq)" ]; then + jq_installed="true" +else + jq_installed="false" +fi + if [ $# = 0 ]; then showHelp; else @@ -158,6 +395,7 @@ else sleep 2 fi fi + while [ $# -gt 0 ]; do arg=$1; @@ -393,6 +631,24 @@ while [ $# -gt 0 ]; do "track" ) showTrack; break ;; + + "liked" ) + array=( $@ ); + len=${#array[@]}; + URIs=${array[@]:2:$len}; + format="tsv"; + getUserAccessToken; + for URI in $URIs; + do + checkLiked; + if [[ $ItIsLiked == "[ true ]" ]];then + cecho "You like: `showURIinfo`" + else + cecho "You don't like (yet): `showURIinfo`" + fi + done + break ;; + esac else # status is the only param @@ -400,6 +656,92 @@ while [ $# -gt 0 ]; do fi break ;; + "list" ) + if [[ $jq_installed == "false" ]];then + echo "Unfortunately the 'list' option will not function without 'jq', a json parser. It can be installed with \"brew install jq\"" + else + if [ $# != 1 ]; then + # There are additional arguments, a status subcommand + if [[ "$3" == "csv" || "$3" == "tsv" || "$3" == "text" || "$3" == "html" ]]; then + format=$3 + else + format="tsv" + fi + case $2 in + "uri" ) + array=( $@ ); + len=${#array[@]}; + URI=${array[@]:2:$len}; + showURIinfo; + break ;; + + "history" ) + getUserAccessToken; + showMyHistory; + break ;; + + "mysongs" | "songs" | "tracks" ) + getUserAccessToken; + likefilter="tracks" + showMyLiked; + break ;; + + "myalbums" | "albums" ) + getUserAccessToken; + likefilter="albums" + showMyLiked; + break ;; + + "mine" ) + getUserAccessToken; + showMyPlaylists; + break ;; + + "devices" ) + getUserAccessToken; + showMyDevices; + break ;; + esac + fi + fi + break ;; + + "like" ) + array=( $@ ); + len=${#array[@]}; + URIs=${array[@]:1:$len}; + format="tsv" + getUserAccessToken; + cecho "You like: " + for URI in $URIs + do + likeURI; + sleep 1 + done + break ;; + + "connect" ) + if [[ $jq_installed == "false" ]];then + echo "Unfortunately the 'connect' option will not function without 'jq', a json parser. It can be installed with \"brew install jq\"" + else + if [ $# != 1 ]; then + # There are additional arguments, a status subcommand + device_id=$2 + getUserAccessToken; + selectDevice; + fi + fi + break ;; + + "queue" | "q" ) + array=( $@ ); + len=${#array[@]}; + URI=${array[@]:1:$len}; + format="tsv" + getUserAccessToken; + queueTrack; + break ;; + "info" ) info=`osascript -e 'tell application "Spotify" set durSec to (duration of current track / 1000) @@ -470,6 +812,11 @@ while [ $# -gt 0 ]; do "help" ) showHelp; break ;; + + "-v" | "--version" ) + showVersion | column -t; + break ;; + * ) showHelp; exit 1;