Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Created September 26, 2020 11:18
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save marco79cgn/75fea28bdb183f79592f2e1875dd5c95 to your computer and use it in GitHub Desktop.
Save marco79cgn/75fea28bdb183f79592f2e1875dd5c95 to your computer and use it in GitHub Desktop.
// widget created by @marco79 (Twitter) user/marco79 (RoutineHub) u/marco79 (Reddit)
// insert your Spotify client id and secret here
const clientId = "xxxxxx"
const clientSecret = "xxxxxx"
const spotifyPlaylistName = "Retrowelle Playlist"
// insert your IFTTT key and webhook url
const iftttUrl = "https://maker.ifttt.com/trigger/add_track_to_spotify/with/key/"
const iftttKey = "xxxxxx"
const logoUrl = "https://www.retrowelle.com/wordpress/wp-content/uploads/2019/03/RETROWELLE-Kreis_WP-Icon.png"
let nowPlaying = await loadNowPlaying()
let widget = await createWidget(nowPlaying)
Script.setWidget(widget)
Script.complete()
widget.presentSmall()
async function createWidget(nowPlaying) {
let widget = new ListWidget()
let artistTitle = nowPlaying.title + ";" + nowPlaying.artist.name
// load last song from iCloud Drive
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "retrowelle-lastsong.txt")
let lastSong = Data.fromFile(path)
let coverPath = fm.joinPath(dir, "retrowelle-cover.jpg")
let uri = ""
let coverUrl = ""
let coverImage
if (lastSong == null || (lastSong != null && lastSong.toRawString().indexOf(artistTitle) == -1)) {
// new song
// Spotify search api query
let result = await searchCoverAtSpotify(nowPlaying.title, nowPlaying.artist.name, true)
if (gotResultFromSpotify(result)) {
let item = result.tracks.items[0]
coverUrl = item.album.images[1].url
uri = item.uri
coverImage = await loadImage(coverUrl)
fm.writeImage(coverPath, coverImage)
} else {
// query spotify again with just one simplified search string
result = await searchCoverAtSpotify(nowPlaying.title, nowPlaying.artist.name, false)
if (gotResultFromSpotify(result)) {
let item = result.tracks.items[0]
coverUrl = item.album.images[1].url
uri = item.uri
coverImage = await loadImage(coverUrl)
fm.writeImage(coverPath, coverImage)
}
}
} else {
// old song
let pos = lastSong.toRawString().lastIndexOf(';') + 1;
uri = lastSong.toRawString().substring(pos, lastSong.toRawString().length)
coverImage = fm.readImage(coverPath)
}
// set cover art background or fallback logo
if (coverImage != null) {
widget.backgroundImage = coverImage
} else {
coverImage = await loadImage(logoUrl)
widget.backgroundImage = coverImage
}
if (uri != null && uri.length > 0) {
widget.url = uri
}
// set gradient background with transparency
let startColor = new Color("#1c1c1c00")
let endColor = new Color("#1c1c1c92")
let gradient = new LinearGradient()
gradient.colors = [startColor, endColor]
gradient.locations = [0.0, 1]
widget.backgroundGradient = gradient
widget.backgroundColor = new Color("1c1c1c")
let updatedAt = new Date().toLocaleTimeString("de-DE", { timeZone: "CET", hour: '2-digit', minute: '2-digit' })
let ts = widget.addText(`${updatedAt} Uhr`)
ts.textColor = Color.white()
ts.font = Font.boldSystemFont(10)
ts.leftAlignText()
widget.addSpacer(2)
// add title and artist
let titleTxt = widget.addText(nowPlaying.title)
titleTxt.font = Font.boldSystemFont(12)
titleTxt.textColor = Color.white()
widget.addSpacer(2)
let artistTxt = widget.addText(nowPlaying.artist.name)
artistTxt.font = Font.boldSystemFont(11)
artistTxt.textColor = Color.yellow()
widget.setPadding(8, 10, 12, 10)
// add track to Spotify playlist using IFTTT webhook
if (args.widgetParameter === "playlist" || config.runsInApp) {
if (lastSong != null) {
if (lastSong.toRawString().indexOf(artistTitle) == -1) {
addToSpotifyPlaylist(nowPlaying.title, nowPlaying.artist.name)
fm.writeString(path, artistTitle + ";" + uri)
} else {
console.log("Song is already part of the playlist")
}
} else {
await addToSpotifyPlaylist(nowPlaying.title, nowPlaying.artist.name)
fm.writeString(path, artistTitle + ";" + uri)
}
}
return widget
}
// helper function to load and parse a restful json api
async function loadNowPlaying() {
const req = new Request("https://api.laut.fm/station/retrowelle/last_songs")
const json = await req.loadJSON()
return json[0]
}
// helper function to download an image from a given url
async function loadImage(imgUrl) {
const req = new Request(imgUrl)
return await req.loadImage()
}
// obtains a spotify search token
async function getSpotifySearchToken() {
let url = "https://accounts.spotify.com/api/token";
let req = new Request(url)
req.method = "POST"
req.body = "grant_type=client_credentials"
let authHeader = "Basic " + btoa(clientId + ":" + clientSecret)
req.headers = { "Authorization": authHeader, "Content-Type": "application/x-www-form-urlencoded" }
let token = await req.loadJSON()
return token.access_token
}
// search for the cover art on Spotify
async function searchCoverAtSpotify(title, artist, strict) {
let searchString
let searchToken = await getCachedSpotifyToken(false)
if (strict === true) {
searchString = encodeURIComponent("track:" + title + " artist:" + artist)
} else {
searchString = encodeURIComponent(artist + " " + title)
}
let searchUrl = "https://api.spotify.com/v1/search?q=" + searchString + "&type=track&market=DE&limit=1"
req = new Request(searchUrl)
req.headers = { "Authorization": "Bearer " + searchToken, "Content-Type": "application/json", "Accept": "application/json" }
let result = await req.loadJSON()
// check if token expired
if (req.response.statusCode == 401) {
searchToken = await getCachedSpotifyToken(true)
req.headers = { "Authorization": "Bearer " + searchToken, "Content-Type": "application/json", "Accept": "application/json" }
result = await req.loadJSON()
}
return result
}
// add track to Spotify playlist using IFTTT webhook
async function addToSpotifyPlaylist(title, artist) {
let cleanTitle = title.split(" (")
let req = new Request(iftttUrl + iftttKey)
req.method = "POST"
req.headers = { "Content-Type": "application/x-www-form-urlencoded" }
req.body = "value1=" + spotifyPlaylistName + "&value2=" + cleanTitle[0] + "&value3=" + artist
let result = await req.loadString()
}
// obtain spotify api search token - either cached or new
async function getCachedSpotifyToken(forceRefresh) {
// load json from iCloud Drive
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "spotify-token.txt")
let contents = Data.fromFile(path)
if (contents != null && contents.toRawString().length > 0 && !forceRefresh) {
console.log("previous token: " + contents.toRawString())
return contents.toRawString()
} else {
console.log("Getting new token from Spotify.")
let token = await getSpotifySearchToken()
fm.writeString(path, token)
return token
}
}
// check whether spotify api search returned a result
function gotResultFromSpotify(result) {
if (result != null && result.tracks != null && result.tracks.items != null && result.tracks.items.length == 1) {
return true
} else {
return false
}
}
@marco79cgn
Copy link
Author

marco79cgn commented Sep 26, 2020

Intro

iOS 14 Custom Widget made with the help of the Scriptable app. It shows the metadata and cover art of a radio station „Radio Retrowelle“ by using the Spotify Search API.
Upon tapping on the widget it opens and plays the song on Spotify. Furthermore it optionally saves every song in a Spotify playlist by using an IFTTT webhook.

Requirements

  • iOS 14
  • Scriptable version 1.5
  • Spotify Web Developer API
    Please create a client to get your client id and client_secret credentials. They are needed to search for cover art.
    Insert them at the top of the script.
  • IFTTT webhook
    Adding songs to a personal Spotify playlist is quite cumbersome when using the official Spotify web API because it needs special permissions. That's why I chose to use IFTTT for that purpose which makes the whole process a lot easier. Here's a detailed description how to create the needed IFTTT webhook.
    You can activate the playlist feature by defining „playlist“ as a widget parameter in the Scriptable widget settings.

Thanks

A big Thank you to @simonbs for making great apps like Scriptable, DataJar or Jayson.

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