Created
September 26, 2020 11:18
-
-
Save marco79cgn/75fea28bdb183f79592f2e1875dd5c95 to your computer and use it in GitHub Desktop.
This file contains 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
// 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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
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.
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.