Last active
October 23, 2023 14:37
-
-
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
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
// 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; | |
} |
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 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
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.
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.