Last active
February 21, 2024 03:26
-
-
Save marco79cgn/dbd9a2b837469d89a36fc5cf9c0436a6 to your computer and use it in GitHub Desktop.
A scriptable iOS widget which displays the program and recent songs of "radioeins vom rbb"
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: orange; icon-glyph: volume-up; | |
// name: radioeins-widget.js | |
// description: A scriptable widget which displays the program and recent songs of radioeins vom rbb | |
// author: Marco Dengel | |
// email: marco79cgn@gmail.com | |
// Insert your Spotify credentials here! | |
const clientId = "******" | |
const clientSecret = "******" | |
const today = dateToYMD(new Date()) | |
let mediathekData; | |
try { | |
mediathekData = await new Request( | |
"http://rbb-cache-1.konsole-labs.com/radioeins/playlist/get/program-day.php?idStation=3&d=" + today | |
).loadJSON(); | |
} catch (e) { | |
const errorWidget = createErrorWidget(); | |
if (!config.runsInWidget) { | |
await errorWidget.presentLarge(); | |
} else { | |
Script.setWidget(errorWidget); | |
} | |
Script.complete(); | |
} | |
const widget = await createWidget(); | |
widget.url = "https://dispatcher.rndfnk.com/rbb/radioeins/live/mp3/mid" | |
if (!config.runsInWidget) { | |
await widget.presentLarge(); | |
} else { | |
Script.setWidget(widget); | |
} | |
Script.complete(); | |
async function createWidget() { | |
let listWidget = new ListWidget(); | |
listWidget.setPadding(20, 20, 0, 20) | |
listWidget.backgroundColor = new Color("#2b2b2b") | |
listWidget = await createHeaderImage(listWidget); | |
listWidget.addSpacer(1); | |
const schedule = await getSchedule() | |
const currentShow = listWidget.addStack() | |
const playNowImage = currentShow.addImage(await getImage('playnow.png')); | |
playNowImage.imageSize = new Size(46,46); | |
currentShow.addSpacer(8) | |
const currentShowInfo = currentShow.addStack() | |
currentShowInfo.layoutVertically() | |
currentShowInfo.addSpacer(5) | |
const nowLive = currentShowInfo.addText("Jetzt live: " + schedule.broadcast.time + " Uhr") | |
nowLive.textColor = Color.white() | |
nowLive.font = Font.boldMonospacedSystemFont(10); | |
const showTitle = currentShowInfo.addText(schedule.broadcast.title) | |
showTitle.textColor = Color.white() | |
showTitle.font = Font.boldMonospacedSystemFont(12); | |
const host = currentShowInfo.addText("mit " + schedule.broadcast.moderation[0].name) | |
host.textColor = Color.white() | |
host.font = Font.boldMonospacedSystemFont(11); | |
host.textOpacity = 0.8 | |
listWidget.addSpacer(2) | |
const line2 = listWidget.addText("_________________________________________________________") | |
line2.font = Font.boldMonospacedSystemFont(8); | |
line2.textColor = Color.white() | |
line2.centerAlignText() | |
listWidget.addSpacer(2) | |
// Get the last three songs | |
let foundSongs = 0 | |
for (let i = 0; i < mediathekData.length; i++) { | |
var currentItem = mediathekData[i] | |
console.log(currentItem) | |
if(currentItem.cl.toLowerCase() === 'music') { | |
listWidget.addSpacer(6) | |
foundSongs += 1 | |
let airtime = currentItem.ts | |
let artist = currentItem.a | |
let title = currentItem.t | |
listWidget = await createPlaylistItem(listWidget, airtime, artist, title); | |
} | |
if(foundSongs > 2) { | |
break | |
} | |
} | |
// next show | |
const line = listWidget.addText("_________________________________________________________") | |
line.font = Font.boldMonospacedSystemFont(8); | |
line.textColor = Color.white() | |
line.centerAlignText() | |
listWidget.addSpacer(6) | |
const nextShow = listWidget.addText("Nächste Sendung: " + schedule.next.time + " Uhr") | |
nextShow.textColor = Color.white() | |
nextShow.font = Font.semiboldMonospacedSystemFont(11); | |
nextShow.centerAlignText() | |
const nextShowDetails = listWidget.addText(schedule.next.title + " mit " + schedule.next.moderation[0].name) | |
nextShowDetails.textColor = Color.white(); | |
nextShowDetails.font = Font.semiboldMonospacedSystemFont(11) | |
nextShowDetails.centerAlignText() | |
const logo = listWidget.addImage(await getImage('radioeins-logo.png')) | |
logo.imageSize = new Size(80, 40) | |
logo.centerAlignImage() | |
return listWidget; | |
} | |
async function createPlaylistItem(listWidget, airtime, artist, title) { | |
let date = new Date(airtime * 1000); | |
let coverImage | |
let uri | |
let coverUrl = "" | |
// Spotify search api query | |
let result = await searchCoverAtSpotify(title, artist, true) | |
if (gotResultFromSpotify(result)) { | |
let item = result.tracks.items[0] | |
coverUrl = item.album.images[1].url | |
uri = item.uri | |
coverImage = await loadImage(coverUrl) | |
} else { | |
// query spotify again with just one simplified search string | |
result = await searchCoverAtSpotify(title, artist, false) | |
if (gotResultFromSpotify(result)) { | |
let item = result.tracks.items[0] | |
coverUrl = item.album.images[1].url | |
uri = item.uri | |
coverImage = await loadImage(coverUrl) | |
} | |
} | |
if(coverImage == null) { | |
coverImage = await getImage("radioeins-default-cover.png") | |
} | |
const playlistItem = listWidget.addStack(); | |
const cover = playlistItem.addImage(coverImage); | |
cover.cornerRadius = 5; | |
cover.imageSize = new Size(48,48) | |
if(uri != null && uri.length > 0) { | |
playlistItem.url = uri | |
} else { | |
playlistItem.url = "spotify://" | |
} | |
playlistItem.addSpacer(6); | |
const songInfo = playlistItem.addStack(); | |
songInfo.layoutVertically(); | |
songInfo.addSpacer(2) | |
const songTitle = songInfo.addText(title); | |
songTitle.textColor = Color.white() | |
songTitle.font = Font.boldMonospacedSystemFont(12); | |
songInfo.addSpacer(2) | |
const songArtist = songInfo.addText(artist); | |
songArtist.textColor = new Color("#ff9f00") | |
songArtist.font = Font.semiboldMonospacedSystemFont(12) | |
songArtist.lineLimit = 1 | |
songInfo.addSpacer(2) | |
if (date) { | |
const airDate = songInfo.addText(formatDate(date)); | |
airDate.font = Font.semiboldMonospacedSystemFont(11); | |
airDate.textOpacity = 0.7; | |
airDate.textColor = Color.white() | |
} | |
return listWidget; | |
} | |
async function createHeaderImage(listWidget) { | |
const headerImage = listWidget.addImage( | |
await getImage('erwachsene-logo.png') | |
); | |
headerImage.imageSize = new Size(100, 16); | |
headerImage.rightAlignImage(); | |
headerImage.applyFillingContentMode(); | |
return listWidget; | |
} | |
function createErrorWidget() { | |
const errorWidget = new ListWidget(); | |
const bgGradient = new LinearGradient(); | |
bgGradient.locations = [0, 1]; | |
bgGradient.colors = [new Color('#2D65AE'), new Color('#19274C')]; | |
errorWidget.backgroundGradient = bgGradient; | |
const title = errorWidget.addText('radioeins vom rbb'); | |
title.font = Font.headline(); | |
title.centerAlignText(); | |
title.textColor = Color.white() | |
errorWidget.addSpacer(10); | |
const errorText = errorWidget.addText( | |
'Es besteht momentan keine Verbindung zum Internet.' | |
); | |
errorText.font = Font.semiboldMonospacedSystemFont(16); | |
errorText.textColor = Color.orange(); | |
errorText.centerAlignText() | |
return errorWidget; | |
} | |
async function loadImage(url) { | |
return await new Request(url).loadImage(); | |
} | |
function formatDate(dateObject) { | |
return `${leadingZero( | |
dateObject.getHours() | |
)}:${leadingZero(dateObject.getMinutes())} Uhr`; | |
} | |
function leadingZero(input) { | |
return ('0' + input).slice(-2); | |
} | |
// random number, min and max included | |
function getRandomNumber(min, max) { | |
return Math.floor(Math.random() * (max - min + 1) + min) | |
} | |
async function getImage(image) { | |
let fm = FileManager.iCloud() | |
let dir = fm.documentsDirectory() | |
let path = fm.joinPath(dir, image) | |
if (fm.fileExists(path)) { | |
return fm.readImage(path) | |
} else { | |
// download once | |
let imageUrl | |
switch (image) { | |
case 'radioeins-logo.png': | |
imageUrl = "https://i.imgur.com/gs96xQ5.png" | |
break | |
case 'erwachsene-logo.png': | |
imageUrl = "https://i.imgur.com/vfnREHs.png" | |
break | |
case 'playnow.png': | |
imageUrl = "https://i.imgur.com/neKo5Cj.png" | |
break | |
case 'radioeins-default-cover.png': | |
imageUrl = "https://static.mytuner.mobi/media/tvos_radios/6pnVM8VR46.png" | |
break | |
default: | |
console.log(`Sorry, couldn't find ${image}.`); | |
} | |
let iconImage = await loadImage(imageUrl) | |
fm.writeImage(path, iconImage) | |
return iconImage | |
} | |
} | |
async function getSchedule() { | |
let url = "https://www.radioeins.de/content/rbb/rad/vorlagen/radiostartmodule-helper.broadcast.jsn"; | |
let req = new Request(url) | |
let schedule = await req.loadJSON() | |
return schedule | |
} | |
// 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 | |
} | |
// 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 | |
} | |
} | |
function dateToYMD(date) { | |
var d = date.getDate(); | |
var m = date.getMonth() + 1; //Month from 0 to 11 | |
var y = date.getFullYear(); | |
return '' + y + '-' + (m<=9 ? '0' + m : m) + '-' + (d <= 9 ? '0' + d : d); | |
} |
Hi marco79cgn, tolle neue Widget-Idee.
Ich hatte mit "Cannot parse response for an image" zu kämpfen.
Lösung bei mir: Eine andere Quelle und .png statt .svg verwenden.
case 'radioeins-logo.png': imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Radio_Eins.svg/320px-Radio_Eins.svg.png" //imageUrl = "https://www.radioeins.de/basis/grafik/RBB_RadioEins_Scholle_sRGB.svg"
@jocheno
Danke für das Feedback. Ich habe es geändert.
Zusätzlich habe ich auch das Design nochmal angepasst, damit lange Sendungstitel nichts kaputt machen.
Am Besten nochmal das ganze Skript oben kopieren. :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Intro
Ein Scriptable iOS widget, das das Programm und die letzten Songs des Senders radioeins vom rbb anzeigt. Die Songs können direkt in Spotify abgespielt werden. Der Livestream kann mit Tap auf das Widget gestartet werden.
Anforderungen
Installation
Disclaimer
Es handelt sich um ein von mir selbst entwickeltes Spaßprojekt, kein offizielles Produkt von radioeins.
Updates
29.07.2023, 10:24
fixed logo and improved layout
28.07.2023, 16:00
Initial Release