Last active
April 11, 2025 23:05
-
-
Save marco79cgn/79a6a265d978dc22cc2a12058b24e02b to your computer and use it in GitHub Desktop.
A Scriptable iOS widget that shows what‘s playing on Spotify
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let spotifyCredentials | |
let widget = await createWidget() | |
Script.setWidget(widget) | |
Script.complete() | |
async function createWidget() { | |
let widget = new ListWidget() | |
let spotifyIcon = await getImage("spotify-icon.png") | |
widget.backgroundColor = new Color("1e2040") | |
// load spotify credentials from iCloud Drive | |
spotifyCredentials = await loadSpotifyCredentials() | |
if(spotifyCredentials != null) { | |
widget.url = "spotify://" | |
let nowPlaying = await loadNowPlaying() | |
if(nowPlaying != null) { | |
widget.setPadding(20,12, 8, 8) | |
let cleanTitle = nowPlaying.item.name.split(" (")[0] | |
cleanTitle = cleanTitle.split(" - ")[0] | |
const artist = nowPlaying.item.artists[0].name | |
// console.log("Now Playing: " + cleanTitle + " - " + artist) | |
// cover art | |
const coverUrl = nowPlaying.item.album.images[0].url | |
let coverImage = await loadImage(coverUrl) | |
let row = widget.addStack() | |
let stack = row.addStack() | |
stack.layoutHorizontally() | |
stack.size = new Size(105,105) | |
let cover = stack.addImage(coverImage) | |
cover.cornerRadius = 6 | |
cover.borderColor = new Color("#1DB954") | |
cover.borderWidth = 3 | |
stack.addSpacer(10) | |
let stack2 = row.addStack() | |
stack2.layoutVertically() | |
let spotifyIconImage = stack2.addImage(spotifyIcon) | |
stack2.addSpacer(10) | |
let shuffleIcon = await getImage("shuffle-icon.png") | |
let shuffleIconImage = stack2.addImage(shuffleIcon) | |
if(nowPlaying.shuffle_state == true) { | |
shuffleIconImage.imageOpacity = 1 | |
} else { | |
shuffleIconImage.imageOpacity = 0.3 | |
} | |
let repeatIcon = await getImage("repeat-icon.png") | |
stack2.addSpacer(10) | |
let repeatIconImage = stack2.addImage(repeatIcon) | |
if(nowPlaying.repeat_state === "off") { | |
repeatIconImage.imageOpacity = 0.3 | |
} else { | |
repeatIconImage.imageOpacity = 1.0 | |
} | |
// add title and artist | |
let titleTxt = widget.addText(cleanTitle) | |
titleTxt.font = Font.semiboldSystemFont(11) | |
titleTxt.textColor = Color.white() | |
titleTxt.lineLimit = 1 | |
widget.addSpacer(2) | |
let artistTxt = widget.addText(artist) | |
artistTxt.font = Font.boldSystemFont(11) | |
artistTxt.textColor = new Color("#1DB954") | |
artistTxt.lineLimit = 1 | |
widget.addSpacer() | |
} else { | |
// Spotify playback stopped | |
let spotifyImage = widget.addImage(spotifyIcon) | |
spotifyImage.imageSize = new Size(25,25) | |
spotifyImage.rightAlignImage() | |
widget.addSpacer(10) | |
let offIcon = await getImage("offline-icon.png") | |
let offImage = widget.addImage(offIcon) | |
offImage.imageSize = new Size(50,50) | |
offImage.centerAlignImage() | |
widget.addSpacer(5) | |
let playbackText = widget.addText("Playback stopped") | |
playbackText.font = Font.semiboldSystemFont(11) | |
playbackText.textColor = Color.white() | |
playbackText.centerAlignText() | |
widget.addSpacer() | |
} | |
} else { | |
// no credentials found | |
let spotifyImage = widget.addImage(spotifyIcon) | |
spotifyImage.imageSize = new Size(25,25) | |
spotifyImage.rightAlignImage() | |
widget.addSpacer(10) | |
console.log("Could not find Spotify credentials!") | |
let ts = widget.addText("Couldn't find your spotify credentials in iCloud Drive. \n\n Please tap me for setup instructions.") | |
ts.textColor = Color.white() | |
ts.font = Font.boldSystemFont(11) | |
ts.leftAlignText() | |
widget.url = "https://gist.github.com/marco79cgn/79a6a265d978dc22cc2a12058b24e02b#gistcomment-3469230" | |
} | |
return widget | |
} | |
// get nowPlaying via Spotify Web API | |
async function loadNowPlaying() { | |
const req = new Request("https://api.spotify.com/v1/me/player") | |
req.headers = { "Authorization": "Bearer " + spotifyCredentials.accessToken, "Content-Type": "application/json" } | |
let npResult = await req.load() | |
if (req.response.statusCode == 401) { | |
// access token expired, trying to refresh | |
let success = await refreshSpotifyAccessToken() | |
if(success) { | |
return await loadNowPlaying() | |
} else { | |
return null | |
} | |
} else if (req.response.statusCode == 204) { | |
// no playback | |
return null | |
} else if (req.response.statusCode == 200) { | |
npResult = JSON.parse(npResult.toRawString()) | |
} | |
return npResult | |
} | |
// load and validate spotify credentials from iCloud Drive | |
async function loadSpotifyCredentials() { | |
let fm = FileManager.iCloud() | |
let dir = fm.documentsDirectory() | |
let path = fm.joinPath(dir, "spotify-credentials.json") | |
let spotifyCredentials | |
if(fm.fileExists(path)) { | |
await fm.downloadFileFromiCloud(path) | |
let spotifyCredentialsFile = Data.fromFile(path) | |
spotifyCredentials = JSON.parse(spotifyCredentialsFile.toRawString()) | |
if (isNotEmpty(spotifyCredentials.clientId) | |
&& isNotEmpty(spotifyCredentials.clientSecret) | |
&& isNotEmpty(spotifyCredentials.accessToken) | |
&& isNotEmpty(spotifyCredentials.refreshToken)) { | |
return spotifyCredentials | |
} | |
} | |
return null | |
} | |
// helper function to check not empty strings | |
function isNotEmpty(stringToCheck) { | |
if (stringToCheck != null && stringToCheck.length > 0) { | |
return true | |
} else { | |
return false | |
} | |
} | |
// The Spotify access token expired so we get a new one by using the refresh token (Authorization Flow) | |
async function refreshSpotifyAccessToken() { | |
if(spotifyCredentials != null) { | |
let req = new Request("https://accounts.spotify.com/api/token") | |
req.method = "POST" | |
req.headers = { "Content-Type": "application/x-www-form-urlencoded" } | |
req.body = "grant_type=refresh_token&refresh_token=" + spotifyCredentials.refreshToken + "&client_id=" + spotifyCredentials.clientId + "&client_secret=" + spotifyCredentials.clientSecret | |
let result = await req.loadJSON() | |
spotifyCredentials.accessToken = result.access_token | |
let fm = FileManager.iCloud() | |
let dir = fm.documentsDirectory() | |
let path = fm.joinPath(dir, "spotify-credentials.json") | |
fm.write(path, Data.fromString(JSON.stringify(spotifyCredentials))) | |
return true | |
} | |
return false | |
} | |
// get images from local filestore or download them once | |
async function getImage(image) { | |
let fm = FileManager.local() | |
let dir = fm.documentsDirectory() | |
let path = fm.joinPath(dir, image) | |
if(fm.fileExists(path)) { | |
return fm.readImage(path) | |
} else { | |
// download once | |
let imageUrl | |
switch (image) { | |
case 'spotify-icon.png': | |
imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Spotify_logo_without_text.svg/240px-Spotify_logo_without_text.svg.png" | |
break | |
case 'shuffle-icon.png': | |
imageUrl = "https://www.iconsdb.com/icons/download/white/shuffle-128.png" | |
break | |
case 'repeat-icon.png': | |
imageUrl = "https://www.iconsdb.com/icons/download/white/repeat-128.png" | |
break | |
case 'offline-icon.png': | |
imageUrl = "http://cdn.1001freedownloads.com/vector/thumb/98366/clarity-shutdown-icon.png" | |
break | |
default: | |
console.log(`Sorry, couldn't find ${image}.`); | |
} | |
let iconImage = await loadImage(imageUrl) | |
fm.writeImage(path, iconImage) | |
return iconImage | |
} | |
} | |
// helper function to download an image from a given url | |
async function loadImage(imgUrl) { | |
const req = new Request(imgUrl) | |
return await req.loadImage() | |
} |
@Firepixel85
I have a Spotify credential file In iCloud Drive it still says this do u have any fix ?
Make sure you have placed the file at the correct path: “iCloud Drive/Scriptables/spotify-credentials.json”. In Additional make sure scriptables has full file access and is able to access the file.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yey it works thank you, I set it up on my standby screen and it looks great