-
-
Save gwthompson/7981f5e7a602f8804d58e4a319db2715 to your computer and use it in GitHub Desktop.
Youtube videos on a widget
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
const YOUTUBE_API = "https://youtube.googleapis.com/youtube/v3" | |
const API_KEY = "YOUR_KEY_HERE" | |
const channelIdArray = [ | |
"UCT6iAerLNE-0J1S_E97UAuQ", // YongYea | |
"UC9PBzalIcEQCsiIkq36PyUA", // Digital Foundry | |
"UCoTIpyf8_RGzJ1LPTmvadaA", // CENTRAL | |
"UCNvzD7Z-g64bPXxGzaQaa4g", // gameranx | |
"UCcGL_0yoZTskvlgAixaEjEg", // whatoplay | |
"UCawsJGDMV6IOm6z9yiOyIsQ", // Cronosfera | |
"UC-zfTtp6tir7yJIrpsgS0dA", // Intoxi Anime | |
"UCBJycsmduvYEL83R_U4JriQ", // Marques Brownlee | |
"UCsBjURrPoezykLs9EqgamOA", // Fireship | |
"UCWFKCr40YwOZQx8FHU_ZqqQ", // JerryRigEverything | |
"UCr6JcgG9eskEzL-k6TtL9EQ", // ZONEofTECH | |
"UCsgv2QHkT2ljEixyulzOnUQ", // AngryJoeShow | |
"UCZ7AeeVbyslLM_8-nVy2B8Q", // Skill Up | |
"UCsvn_Po0SmunchJYOWpOxMg", // videogamedunkey | |
"UCVYamHliCI9rw1tHR1xbkfw", // Dave2D | |
] | |
// 120 minutes interval | |
const refreshInterval = (120*60*1000) | |
try { | |
const videos = await getRecentYoutubeVideos(channelIdArray, getMaxQuantityAllowed()) | |
const widget = videos.length > 0 | |
? await createVideoWidget(videos) | |
: getWidgetWithMessage({ | |
message: "Nothing new here", | |
secondaryMessage: "No video published in the last 24 hours", | |
image: SFSymbol.named("video.bubble.left").image | |
}) | |
widget.refreshAfterDate = new Date(Date.now() + (90*60*1000)) | |
renderWidget(widget) | |
} catch(error) { | |
const widget = await createErrorWidget(error) | |
renderWidget(widget) | |
} | |
async function createVideoWidget(videos) { | |
const widget = new ListWidget() | |
setBackground(widget) | |
setTitleStack(widget) | |
widget.addSpacer(5) | |
await createVideoGrid(widget, videos) | |
widget.addSpacer() | |
widget.url = "https://www.youtube.com/feed/subscriptions" | |
return widget | |
} | |
async function createVideoGrid(widget, videos) { | |
const gridStack = widget.addStack() | |
gridStack.layoutVertically() | |
for (const video of videos) { | |
await addVideoToGridStack(gridStack, video) | |
} | |
} | |
async function addVideoToGridStack(gridStack, video) { | |
const {snippet, id} = video | |
const {title, thumbnails, publishedAt, channelTitle} = snippet | |
const videoStack = gridStack.addStack() | |
videoStack.layoutHorizontally() | |
videoStack.addSpacer() | |
videoStack.url = `https://www.youtube.com/watch?v=${id.videoId}` | |
const coverElement = videoStack.addImage(await loadImageFromUrl(thumbnails.medium.url)) | |
coverElement.imageSize = new Size(90, 60) | |
coverElement.cornerRadius = 10 | |
videoStack.addSpacer(10) | |
const contentStack = videoStack.addStack() | |
contentStack.layoutVertically() | |
contentStack.size = new Size(210, 0) | |
contentStack.addSpacer(3) | |
const titleStack = contentStack.addStack() | |
titleStack.size = new Size(0, 32) | |
const titleElement = titleStack.addText(title) | |
titleElement.lineLimit = 2 | |
titleElement.font = Font.boldSystemFont(13) | |
contentStack.addSpacer(7) | |
const additionalInfoStack = contentStack.addStack() | |
const channelNameElement = additionalInfoStack.addText(channelTitle) | |
channelNameElement.font = Font.semiboldSystemFont(11) | |
channelNameElement.lineLimit = 1 | |
additionalInfoStack.addSpacer() | |
const hourDifference = getHourDifference(new Date(publishedAt)) | |
const publishedTimeElement = additionalInfoStack.addText( | |
hourDifference > 1 | |
? `${getHourDifference(new Date(publishedAt))} hours ago` | |
: `Just now` | |
) | |
publishedTimeElement.font = Font.semiboldSystemFont(11) | |
videoStack.addSpacer() | |
} | |
async function getRecentYoutubeVideos(channelIdArray, limit) { | |
let videoArray = [] | |
const today = new Date() | |
const yesterday = new Date() | |
yesterday.setDate(yesterday.getDate() - 1) | |
for(const channelId of channelIdArray) { | |
if (videoArray.length < limit) { | |
const request = new Request(`${YOUTUBE_API}/search?part=snippet&key=${API_KEY}&channelId=${channelId}&publishedAfter=${formatDateToRFC(yesterday)}&publishedBefore=${formatDateToRFC(today)}`) | |
const response = await request.loadJSON() | |
if (response.error) { | |
throw new Error(response.error.message) | |
} | |
if (response.items) { | |
videoArray = [...videoArray, ...response.items] | |
} | |
} | |
} | |
videoArray.sort((v1, v2) => new Date(v2.snippet.publishedAt) - new Date(v1.snippet.publishedAt)) | |
return videoArray.length > limit | |
? videoArray.slice(0, limit) | |
: videoArray | |
} | |
async function setTitleStack(widget) { | |
const titleStack = widget.addStack() | |
titleStack.size = new Size(330, 15) | |
const dateFormatter = new DateFormatter() | |
dateFormatter.dateFormat = "HH:mm" | |
const lastUpdate = titleStack.addText(`Last update: ${dateFormatter.string(new Date())}`) | |
lastUpdate.font = Font.boldSystemFont(13) | |
} | |
function createErrorWidget(error) { | |
return getWidgetWithMessage({ | |
message: "Cannot load videos", | |
secondaryMessage: error.toString().replace("Error: ", ""), | |
image: SFSymbol.named("wifi.exclamationmark").image, | |
}) | |
} | |
function getWidgetWithMessage({message, secondaryMessage, image}) { | |
const widget = new ListWidget() | |
setBackground(widget, {}) | |
widget.addSpacer() | |
const contentStack = widget.addStack() | |
contentStack.layoutHorizontally() | |
contentStack.addSpacer() | |
const imageElement = contentStack.addImage(image) | |
imageElement.tintColor = Color.white() | |
imageElement.imageSize = new Size(27, 27) | |
contentStack.addSpacer() | |
const textElement = contentStack.addText(message) | |
textElement.font = Font.systemFont(23) | |
contentStack.addSpacer() | |
widget.addSpacer(10) | |
const errorDetailStack = widget.addStack() | |
errorDetailStack.layoutHorizontally() | |
errorDetailStack.addSpacer() | |
const errorDetailElement = errorDetailStack.addText(secondaryMessage) | |
errorDetailElement.font = Font.systemFont(13) | |
errorDetailStack.addSpacer() | |
widget.addSpacer() | |
return widget | |
} | |
async function setBackground(widget) { | |
widget.backgroundColor = new Color("ad0303") | |
} | |
function renderWidget(widget) { | |
if (config.runsInWidget) { | |
Script.setWidget(widget) | |
} else { | |
widget.presentMedium() | |
} | |
Script.complete() | |
} | |
function getMaxQuantityAllowed() { | |
switch(config.widgetFamily) { | |
case 'small': | |
return 1 | |
case 'medium': | |
return 2 | |
case 'large': | |
return 5 | |
default: | |
return 2 | |
} | |
} | |
async function loadImageFromUrl(url) { | |
const req = new Request(url) | |
return req.loadImage() | |
} | |
function formatDateToRFC(date) { | |
const dateFormatter = new DateFormatter() | |
dateFormatter.dateFormat = "yyyy-MM-dd" | |
const timeFormatter = new DateFormatter() | |
timeFormatter.dateFormat = "HH:mm:ss" | |
return `${dateFormatter.string(date)}T${timeFormatter.string(date)}Z` | |
} | |
function getHourDifference(date) { | |
return Math.floor(Math.abs(new Date() - date) / 3.6e6) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment