Skip to content

Instantly share code, notes, and snippets.

@29SimonB
Last active June 16, 2024 22:32
Show Gist options
  • Save 29SimonB/a24b6a16aee17b1279fbb8dc6e436f95 to your computer and use it in GitHub Desktop.
Save 29SimonB/a24b6a16aee17b1279fbb8dc6e436f95 to your computer and use it in GitHub Desktop.
A Scriptable iOS widget that shows what‘s playing on Spotify
//Source: Spotify Web API
let spotifyCredentials
if (!config.runsInWidget) {
switch (config.widgetFamily) {
case 'small': await widget.presentSmall(); break;
case 'medium': await widget.presentMedium(); break;
case 'large': await widget.presentLarge(); break;
}
} else {
if(config.widgetFamily == "small") {
let widget = await createSmallWidget()
Script.setWidget(widget)
}
if(config.widgetFamily == "medium") {
let widget = await createMediumWidget()
Script.setWidget(widget)
}
}
Script.complete()
async function createSmallWidget() {
let list = new ListWidget()
list.backgroundColor = new Color("#191414")
let spotifyIcon = await getImage("spotify-icon.png")
let pauseIcon = await getImage("pause-icon.png")
let playIcon = await getImage("play-icon.png")
let squareIcon = await getImage("square-icon.png")
// load spotify credentials from iCloud Drive
spotifyCredentials = await loadSpotifyCredentials()
if(spotifyCredentials != null) {
list.url = "spotify://"
let nowPlaying = await loadNowPlaying()
if(nowPlaying != null) {
list.setPadding(20,12, 8, 8)
let cleanTitle = nowPlaying.item.name.split(" (")[0]
cleanTitle = cleanTitle.split(" - ")[0]
const artist = nowPlaying.item.artists[0].name
// cover art
const coverUrl = nowPlaying.item.album.images[0].url
let coverImage = await loadImage(coverUrl)
let stack = list.addStack()
stack.layoutHorizontally()
stack.size = new Size(130,105)
let cover = stack.addImage(coverImage)
cover.cornerRadius = 10
cover.borderColor = new Color("#1DB954")
cover.borderWidth = 3
stack.addSpacer(10)
let stack1 = stack.addStack()
stack1.size = new Size(25,100)
stack1.layoutVertically()
let logoStack = stack1.addStack()
logoStack.size = new Size(25,25)
let spotifyIconImage = logoStack.addImage(spotifyIcon)
stack1.addSpacer(55)
let playerStack = stack1.addStack()
playerStack.size = new Size(25,15)
let pauseIconImage = playerStack.addImage(pauseIcon)
stack1.addSpacer(5)
// add title and artist
let titleTxt = list.addText(cleanTitle)
titleTxt.font = Font.semiboldSystemFont(11)
titleTxt.textColor = new Color("#1DB954")
titleTxt.lineLimit = 1
list.addSpacer(2)
let artistTxt = list.addText(artist)
artistTxt.font = Font.boldSystemFont(11)
artistTxt.textColor = new Color("#9e9e9e")
artistTxt.lineLimit = 1
list.addSpacer(10)
} else {
// Spotify playback stopped
list.setPadding(20,12, 8, 8)
// cover art
let stack = list.addStack()
stack.layoutHorizontally()
stack.size = new Size(130,105)
let cover = stack.addImage(squareIcon)
cover.cornerRadius = 10
cover.borderColor = new Color("#1DB954")
cover.borderWidth = 3
stack.addSpacer(10)
//add spotify icon
let stack1 = stack.addStack()
stack1.size = new Size(25,100)
stack1.layoutVertically()
let spotifyIconImage = stack1.addImage(spotifyIcon)
stack1.addSpacer(55)
let stack2 = stack1.addStack()
stack2.size = new Size(25,15)
let playIconImage = stack2.addImage(playIcon)
stack1.addSpacer(5)
// add title and artist
let titleTxt = list.addText("Spotify Player")
titleTxt.font = Font.semiboldSystemFont(11)
titleTxt.textColor = new Color("#1DB954")
titleTxt.lineLimit = 1
list.addSpacer(2)
let artistTxt = list.addText("Click to open")
artistTxt.font = Font.boldSystemFont(11)
artistTxt.textColor = new Color("#9e9e9e")
artistTxt.lineLimit = 1
list.addSpacer(10)
}
} else {
// no credentials found
let errorStack = list.addStack()
errorStack.size = new Size(130,130)
errorStack.layoutVertically()
let logoStack = errorStack.addStack()
logoStack.size = new Size(130,30)
logoStack.addSpacer()
let spotifyImage = logoStack.addImage(spotifyIcon)
spotifyImage.imageSize = new Size(25,25)
spotifyImage.rightAlignImage()
errorStack.addSpacer()
let textStack = errorStack.addStack()
textStack.size = new Size(130,80)
let ts = textStack.addText("Couldn't find your spotify credentials in iCloud Drive. \n\nPlease tap me for setup instructions.")
ts.textColor = Color.white()
ts.font = Font.boldSystemFont(11)
errorStack.addSpacer()
console.log("Could not find Spotify credentials!")
list.url = "https://gist.github.com/SimonBoer/a24b6a16aee17b1279fbb8dc6e436f95#gistcomment-3593498"
}
return list
}
async function createMediumWidget() {
let list = new ListWidget()
list.backgroundColor = new Color("#191414")
let spotifyIcon = await getImage("spotify-icon.png")
let pauseIcon = await getImage("pause-icon.png")
let playIcon = await getImage("play-icon.png")
let squareIcon = await getImage("square-icon.png")
let shuffleIcon = await getImage("shuffle-icon.png")
let repeatIcon = await getImage("repeat-icon.png")
// load spotify credentials from iCloud Drive
spotifyCredentials = await loadSpotifyCredentials()
if(spotifyCredentials != null) {
list.url = "spotify://"
let nowPlaying = await loadNowPlaying()
if(nowPlaying != null) {
list.setPadding(20,10,5,5)
let cleanTitle = nowPlaying.item.name.split(" (")[0]
cleanTitle = cleanTitle.split(" - ")[0]
const artist = nowPlaying.item.artists[0].name
// cover art
const coverUrl = nowPlaying.item.album.images[0].url
let coverImage = await loadImage(coverUrl)
let row = list.addStack()
row.size = new Size(310,145)
let cover = row.addImage(coverImage)
cover.cornerRadius = 10
cover.borderColor = new Color("#1DB954")
cover.borderWidth = 3
cover.imageSize = new Size(130,130)
row.addSpacer(10)
let stack = row.addStack()
stack.layoutHorizontally()
stack.size = new Size(170,130)
let stack1 = stack.addStack()
stack1.layoutVertically()
stack1.addSpacer()
// add title and artist
let titleTxt = stack1.addText(cleanTitle)
titleTxt.font = Font.semiboldSystemFont(11)
titleTxt.textColor = new Color("#1DB954")
titleTxt.lineLimit = 1
stack1.addSpacer(2)
let artistTxt = stack1.addText(artist)
artistTxt.font = Font.boldSystemFont(11)
artistTxt.textColor = new Color("#9e9e9e")
artistTxt.lineLimit = 1
stack1.addSpacer(55)
//add player icons
let playerStack = stack1.addStack()
playerStack.layoutHorizontally()
playerStack.addSpacer(10)
let shuffleIconImage = playerStack.addImage(shuffleIcon)
if(nowPlaying.shuffle_state == true) {
shuffleIconImage.imageOpacity = 1.0
} else {
shuffleIconImage.imageOpacity = 0.3
}
playerStack.addSpacer()
let pauseIconImage = playerStack.addImage(pauseIcon)
pauseIconImage.leftAlignImage()
playerStack.addSpacer()
let repeatIconImage = playerStack.addImage(repeatIcon)
if(nowPlaying.repeat_state === "off") {
repeatIconImage.imageOpacity = 0.3
} else {
repeatIconImage.imageOpacity = 1.0
}
stack1.addSpacer(20)
stack.addSpacer()
//add spotify icon
let stack2 = stack.addStack()
stack2.size = new Size(30,105)
stack2.layoutVertically()
let logoStack = stack2.addStack()
logoStack.size = new Size(25,25)
let spotifyIconImage = logoStack.addImage(spotifyIcon)
stack2.addSpacer()
} else {
// Spotify playback stopped
list.setPadding(20,10,5,5)
// cover art
let row = list.addStack()
row.size = new Size(310,145)
let cover = row.addImage(squareIcon)
cover.cornerRadius = 10
cover.borderColor = new Color("#1DB954")
cover.borderWidth = 3
cover.imageSize = new Size(130,130)
row.addSpacer(10)
let stack = row.addStack()
stack.layoutHorizontally()
stack.size = new Size(170,130)
let stack1 = stack.addStack()
stack1.layoutVertically()
stack1.addSpacer()
// add title and artist
let titleTxt = stack1.addText("Spotify Player")
titleTxt.font = Font.semiboldSystemFont(11)
titleTxt.textColor = new Color("#1DB954")
titleTxt.lineLimit = 1
stack1.addSpacer(2)
let artistTxt = stack1.addText("Click to open")
artistTxt.font = Font.boldSystemFont(11)
artistTxt.textColor = new Color("#9e9e9e")
artistTxt.lineLimit = 1
stack1.addSpacer(55)
//add player icons
let playerStack = stack1.addStack()
playerStack.layoutHorizontally()
playerStack.addSpacer(10)
let shuffleIconImage = playerStack.addImage(shuffleIcon)
playerStack.addSpacer()
let playIconImage = playerStack.addImage(playIcon)
playerStack.addSpacer()
let repeatIconImage = playerStack.addImage(repeatIcon)
stack1.addSpacer(20)
stack.addSpacer()
//add spotify icon
let stack2 = stack.addStack()
stack2.size = new Size(30,105)
stack2.layoutVertically()
let logoStack = stack2.addStack()
logoStack.size = new Size(25,25)
let spotifyIconImage = logoStack.addImage(spotifyIcon)
stack2.addSpacer()
}
} else {
// no credentials found
let errorStack = list.addStack()
errorStack.size = new Size(310,145)
errorStack.layoutHorizontally()
let textStack = errorStack.addStack()
textStack.size = new Size(275,130)
textStack.layoutVertically()
let ts = textStack.addText("Couldn't find your spotify credentials in iCloud Drive. \n\nPlease tap me for setup instructions.")
ts.textColor = Color.white()
ts.font = Font.boldSystemFont(11)
ts.leftAlignText()
let logoStack = errorStack.addStack()
logoStack.layoutVertically()
logoStack.size = new Size(25,130)
let spotifyImage = logoStack.addImage(spotifyIcon)
spotifyImage.imageSize = new Size(25,25)
console.log("Could not find Spotify credentials!")
list.url = "https://gist.github.com/SimonBoer/a24b6a16aee17b1279fbb8dc6e436f95#gistcomment-3593498"
}
return list
}
// 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.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, image)
if(fm.fileExists(path)) {
await fm.downloadFileFromiCloud(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 'pause-icon.png':
imageUrl = "https://www.iconsdb.com/icons/download/white/pause-128.png"
break
case 'play-icon.png':
imageUrl = "https://www.iconsdb.com/icons/download/white/play-6-128.png"
break
case 'square-icon.png':
imageUrl = "https://www.iconsdb.com/icons/download/white/square-256.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()
}
@29SimonB
Copy link
Author

29SimonB commented Jan 14, 2021

722F829A-D7EF-46B0-8061-75440A3AE81D

Intro

iOS 14 Custom Widget for the Scriptable app. It shows what‘s currently being played on Spotify with the help of the official Spotify Web API. Upon tapping on the widget it opens the Spotify app.

Requirements

  • iOS 14
  • Scriptable version 1.6 (or higher)
  • Spotify Web Developer API
    Please create a client to get your client id and client_secret credentials. They are needed for the widget. Furthermore you have to grant permissions to the client to read your Spotify‘s playback state. This has to be done only once and involves a Browser/Safari. There is a Siri Shortcut which simplifies the whole process of the Authorization Code Flow.

How to use the Siri shortcut:

  1. Download and install it
  2. Provide your Spotify client id and client_secret to the import wizard
  3. Run the shortcut
  4. a Spotify web page will be opened asking you to grant the needed permissions, click „Agree“ at the bottom
  5. you will be redirected to a example.com webpage - don‘t close it yet
  6. click on the sharing button at the bottom of the embedded Safari window and choose „copy“
  7. now close the browser window by pushing „Done“ on the upper left
  8. when asked for a folder to save the credentials file, choose the Scriptable folder in your iCloud Drive

That‘s it. At the end there will be a file named spotify-credentials.json inside your Scriptable folder in iCloud Drive with the following content:
{ "clientSecret":"xxx", "clientId":"xxx", "accessToken":"xxx", "refreshToken":"xxx" }
Make sure to keep this sensitive information safe and don‘t share it anywhere.

Background: The access token is needed by the widget to get your current playback information. The token expires after one hour but the widget is able to detect an expired token and will automatically refresh the token (with the help of the refresh token, client id and secret).

Widget Installation

  1. Copy the source code from above (click on "raw" in the upper right corner)
  2. Open the Scriptable app
  3. Click on the "+" symbol in the upper right corner and paste the copied script into it
  4. Click on the title of the script at the top and give it a name (e.g. Spotify Player Widget)
  5. Save the script by clicking on "Done" in the top left corner.
  6. Go to your iOS/iPadOS homescreen and press anywhere to enter "wiggle mode" (which also allows you to arrange the app icons).
  7. Press the "+" icon on the top left, then scroll down to "Scriptable" (list is alphabetical), select the widget size you want to display and press "Add Widget" at the bottom.
  8. Press on the widget to edit its settings (optionally long press if wiggle mode has already been exited)
  9. Under "Script" select the one created above (Spotify Player Widget)

Caveat

Currently there‘s no chance to force an update of the widget‘s content. Only iOS decides when exactly widgets are updated. It‘s approximately every 5 minutes but it depends on different factors (like the battery state, low power mode, etc.). As a consequence, the content of the widget might be outdated or lagging behind. Until this update behavior changes, the widget is a proof of concept.

Plans

In my next Version I planned to make a automatic Dark Mode switch that adapts to the system settings of your device but it is hard to code that. So maybe someone have a tip or a tamplate.

Thanks

This description is copied from the original Widget Code by marco79cgn.
I only edit the code a little bit. So thanks for the great widget marco79cgn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment