Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Last active February 21, 2024 03:26
Show Gist options
  • Save marco79cgn/dbd9a2b837469d89a36fc5cf9c0436a6 to your computer and use it in GitHub Desktop.
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"
// 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);
}
@marco79cgn
Copy link
Author

marco79cgn commented Jul 28, 2023

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

  • Kopiere den Source code von oben (klick)
  • Öffne die Scriptable app auf dem iPhone/iPad
  • Klick auf das "+" Symbol oben rechts und füge das kopierte Skript ein
  • Ersetze ganz oben deine Spotify Credentials (client id und secret) für die Suche
  • Klick auf den Titel des Skripts ganz oben und vergebe einen Namen (z.B. Radioeins-Widget)
  • Speichere das Skript durch Klick auf "Done" oben links
  • Gehe auf deinen iOS Homescreen und drücke irgendwo lang, um in den "wiggle mode" zu kommen (mit dem man auch die App Symbole anordnen kann)
  • Drücke das "+" Symbol oben links, blättere dann nach unten zu "Scriptable" (Liste ist alphabetisch), wähle die erste Widget Größe (small) und drück unten auf "Widget hinzufügen"
  • Drücke auf das Widget, um seine Einstellungen zu bearbeiten (optional lang drücken, wenn der Wiggle Modus schon beendet wurde)
  • Wähle unter "Script" das oben erstellte aus (Radioeins-Widget)

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

@jocheno
Copy link

jocheno commented Jul 29, 2023

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"

@marco79cgn
Copy link
Author

marco79cgn commented Jul 29, 2023

@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