Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Last active October 23, 2023 14:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marco79cgn/8e23811c716d405f170bd60a24c68d62 to your computer and use it in GitHub Desktop.
Save marco79cgn/8e23811c716d405f170bd60a24c68d62 to your computer and use it in GitHub Desktop.
A Scriptable iOS widget that shows the last 5 positions of the SWR1 Top 1000 charts
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: newspaper;
// insert your Spotify client id and secret here (replace xxx)
const clientId = "xxx"
const clientSecret = "xxx"
let widget = new ListWidget()
widget.setPadding(22, 10, 10, 10)
widget.url = "https://www.swr-vote.de/swr1-hitparade-rheinland-pfalz-2023"
const logoUrl = "https://www.radiowoche.de/wp-content/uploads/2016/10/logo_SWR1.png"
// 128C7E
const widgetBackground = new Color("#1e2d47") //Widget Background
const stackBackground = new Color("#FFFFFF") //Smaller Container Background
const calendarColor = new Color("#EA3323") //Calendar Color
widget.backgroundColor = widgetBackground
const stackSize = new Size(149, 45) //0 means its automatic//
let image = await loadImage(logoUrl)
const date = new Date()
const dateNow = Date.now()
let df_Name = new DateFormatter()
let df_Month = new DateFormatter()
df_Name.dateFormat = "EEEE"
df_Month.dateFormat = "MMMM"
const dayName = df_Name.string(date)
const dayNumber = date.getDate().toString()
const monthName = df_Month.string(date)
//Top Row (Date & Weather)
let topRow = widget.addStack()
topRow.layoutHorizontally()
//Top Row Date
let dateStack = topRow.addStack()
dateStack.layoutHorizontally()
dateStack.centerAlignContent()
dateStack.setPadding(7, 7, 7, 7)
dateStack.backgroundColor = stackBackground
dateStack.cornerRadius = 4
dateStack.size = stackSize
dateStack.addSpacer()
let dayNumberTxt = dateStack.addText(dayNumber + ".")
dayNumberTxt.font = Font.semiboldSystemFont(26)
dayNumberTxt.textColor = Color.black()
dateStack.addSpacer(7)
let dateTextStack = dateStack.addStack()
dateTextStack.layoutVertically()
let monthNameTxt = dateTextStack.addText(monthName.toUpperCase())
monthNameTxt.font = Font.boldSystemFont(10)
monthNameTxt.textColor = Color.black()
let dayNameTxt = dateTextStack.addText(dayName)
dayNameTxt.font = Font.boldSystemFont(11)
dayNameTxt.textColor = new Color("#f20000")
dateStack.addSpacer()
topRow.addSpacer(6)
let logoStack = topRow.addStack()
logoStack.layoutHorizontally()
logoStack.centerAlignContent()
logoStack.setPadding(7, 7, 7, 7)
logoStack.backgroundColor = stackBackground
logoStack.cornerRadius = 4
logoStack.size = stackSize
let widgetImage = logoStack.addImage(image)
widgetImage.imageSize = new Size(96, 38)
widgetImage.centerAlignImage()
widget.addSpacer(5)
let lastSongsJson = new Object()
await loadLastSongs()
Script.setWidget(widget)
Script.complete()
widget.presentLarge()
// helper function to load and parse a restful json api
async function loadLastSongs() {
let cachedSongs = await loadCachedSongs()
let url = 'https://api.swr-vote.de/event/e2b9d603-07b2-452d-b87e-b0e4a60497c9/ladder?limit=5&offset=0'
let req = new Request(url)
// Parse the values into a JSON file
let result = await req.loadJSON()
let items = result.data
console.log("items: " + items)
for (var i = 0; i < 5; i++) {
var currentItem = items[i]
var currentPosition = currentItem.place
var currentArtist = currentItem.artist
if (currentArtist.indexOf(", ") > 0) {
currentArtist = currentArtist.split(", ")[1] + " " + currentArtist.split(", ")[0]
}
var currentTitle = currentItem.title
console.log("currentArtist:" + currentArtist)
let titleBase64 = hashCode(currentTitle)
let coverImage
let uri
if (cachedSongs.hasOwnProperty(titleBase64)) {
currentArtist = cachedSongs[titleBase64].artist
currentPosition = cachedSongs[titleBase64].position
coverImage = await loadCachedImage(titleBase64)
uri = cachedSongs[titleBase64].uri
} else {
let coverUrl = ""
// Spotify search api query
let result = await searchCoverAtSpotify(currentTitle, currentArtist, true)
if (gotResultFromSpotify(result)) {
let item = result.tracks.items[0]
if (item.album.images[1]) {
coverUrl = item.album.images[1].url
} else {
coverUrl = logoUrl
}
uri = item.uri
coverImage = await loadImage(coverUrl)
} else {
// query spotify again with just one simplified search string
result = await searchCoverAtSpotify(currentTitle, currentArtist, false)
if (gotResultFromSpotify(result)) {
let item = result.tracks.items[0]
if (item.album.images[1]) {
coverUrl = item.album.images[1].url
} else {
coverUrl = logoUrl
}
uri = item.uri
coverImage = await loadImage(coverUrl)
}
}
if (coverImage == null) {
coverImage = await loadImage(logoUrl)
}
await saveAlbumCover(titleBase64, coverImage)
} // end else
// create content in widget
let currentItemStack = widget.addStack()
currentItemStack.layoutHorizontally()
currentItemStack.backgroundColor = stackBackground
currentItemStack.cornerRadius = 4
currentItemStack.size = new Size(306, 50)
let coverStack = currentItemStack.addStack()
coverStack.centerAlignContent()
coverStack.layoutVertically()
coverStack.addSpacer(2)
let cover = coverStack.addImage(coverImage)
cover.imageSize = new Size(46, 46)
cover.cornerRadius = 4
cover.centerAlignImage()
currentItemStack.addSpacer(6)
let currentSongStack = currentItemStack.addStack()
currentSongStack.layoutVertically()
currentSongStack.size = new Size(250, 50)
currentSongStack.setPadding(3, 3, 3, 3)
let positionText = currentSongStack.addText("Platz " + currentPosition)
positionText.font = Font.semiboldSystemFont(10)
positionText.textOpacity = 0.8
let titleText = currentSongStack.addText(currentTitle)
titleText.font = Font.boldSystemFont(13)
titleText.minimumScaleFactor = 0.5
let artistText = currentSongStack.addText(currentArtist)
artistText.font = Font.semiboldSystemFont(12)
artistText.textOpacity = 1
artistText.textColor = Color.darkGray()
if (uri != null && uri.length > 0) {
currentItemStack.url = uri
} else {
currentItemStack.url = "spotify://"
}
widget.addSpacer(5)
createJsonEntry(titleBase64, currentTitle, currentArtist, currentPosition, uri)
}
widget.addSpacer()
await saveLastSongs(JSON.stringify(lastSongsJson))
}
// helper function to download an image
async function loadImage(url) {
let req = new Request(url)
return await req.loadImage()
}
function createDivider() {
const drawContext = new DrawContext()
drawContext.size = new Size(543, 1)
const path = new Path()
path.addLine(new Point(1000, 20))
drawContext.addPath(path)
drawContext.setStrokeColor(new Color("#fff", 1))
drawContext.setLineWidth(1)
drawContext.strokePath()
return drawContext.getImage()
}
// gets 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
}
// 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) {
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
}
}
async function loadCachedSongs() {
// load last song from iCloud Drive
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "swr1-lastsongs.txt")
let lastSongs = Data.fromFile(path)
if (lastSongs != null) {
return JSON.parse(lastSongs.toRawString())
} else {
return new Object()
}
}
async function loadCachedImage(imageName) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "images")
let imagePath = fm.joinPath(path, imageName + ".png")
return fm.readImage(imagePath)
}
async function saveLastSongs(lastSongsJson) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "swr1-lastsongs.txt")
fm.writeString(path, lastSongsJson)
}
async function saveAlbumCover(filename, cover) {
console.log("writing cover " + filename + " to iCloud.")
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "images")
let imagePath = fm.joinPath(path, filename + ".png")
fm.writeImage(imagePath, cover)
}
function createJsonEntry(titleB64, title, artist, position, uri) {
var item = new Object()
item.title = title
item.artist = artist
item.position = position
if (uri != null && uri.length > 0) {
item.uri = uri
} else {
item.uri = ""
}
lastSongsJson[titleB64] = item
}
function hashCode(string) {
var hash = 0;
if (string.length == 0) return hash;
for (i = 0; i < string.length; i++) {
char = string.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
@marco79cgn
Copy link
Author

marco79cgn commented Oct 19, 2020

Intro

iOS 14 Custom Widget made with the help of the Scriptable app. It shows the last 5 positions of the SWR1 Top 1000 charts incl. cover art by using the Spotify Search API.
Upon tapping on a song it opens and plays it on Spotify.

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.
  • Preparations
    Create a folder named "images" inside your iCloud Drive Scriptable folder. Open the Scriptable app on your iPhone, click on the "+" sign on the upper right, copy the code above and paste it inside. Insert your Spotify client id and secret at the top. Save the script by pressing "Done" in the upper left. Go to your homescreen, long press anywhere and configure a new Scriptable widget with maximum size. Assign the created widget.

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