Last active
August 19, 2021 08:37
-
-
Save brenoprata10/a7bfb8df46a547fdb6215bc12d147be5 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_API_KEY_HERE" | |
const channelIdArray = [ | |
"UCT6iAerLNE-0J1S_E97UAuQ", // YongYea | |
"UC9PBzalIcEQCsiIkq36PyUA", // Digital Foundry | |
"UCNvzD7Z-g64bPXxGzaQaa4g", // gameranx | |
"UCcGL_0yoZTskvlgAixaEjEg", // whatoplay | |
"UCawsJGDMV6IOm6z9yiOyIsQ", // Cronosfera | |
"UC-zfTtp6tir7yJIrpsgS0dA", // Intoxi Anime | |
"UCBJycsmduvYEL83R_U4JriQ", // Marques Brownlee | |
"UCsBjURrPoezykLs9EqgamOA", // Fireship | |
"UCWFKCr40YwOZQx8FHU_ZqqQ", // JerryRigEverything | |
"UCr6JcgG9eskEzL-k6TtL9EQ", // ZONEofTECH | |
"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()) | |
log(videos) | |
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) | |
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.replace(/'/g, "'") | |
.replace(/&/g, '&') | |
.replace(/"/g, '\\"') | |
) | |
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 | |
? `${hourDifference} hours ago` | |
: `Just now` | |
) | |
publishedTimeElement.font = Font.semiboldSystemFont(11) | |
videoStack.addSpacer() | |
} | |
async function getRecentYoutubeVideos(channelIdArray, limit) { | |
const fileManager = FileManager.local() | |
const cacheFilePath = `${fileManager.cacheDirectory()}/youtube-cache.json` | |
const cachedVideos = fileManager.fileExists(cacheFilePath) | |
? JSON.parse(fileManager.readString(cacheFilePath)) | |
: [] | |
log(cachedVideos) | |
let videoArray = [ | |
...cachedVideos.filter((video) => getHourDifference(new Date(video.snippet.publishedAt)) < 20) | |
] | |
const today = new Date() | |
const yesterday = new Date() | |
yesterday.setDate(yesterday.getDate() - 1) | |
for(const channelId of channelIdArray) { | |
const isChannelIdNotCached = cachedVideos.every((video) => video.snippet.channelId !== channelId) | |
if (videoArray.length < limit && isChannelIdNotCached) { | |
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() | |
log(channelId) | |
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)) | |
const recentVideos = videoArray.length > limit | |
? videoArray.slice(0, limit) | |
: videoArray | |
fileManager.writeString(cacheFilePath, JSON.stringify(recentVideos)) | |
return recentVideos | |
} | |
async function setTitleStack(widget) { | |
const titleStack = widget.addStack() | |
titleStack.size = new Size(330, 15) | |
const dateFormatter = new DateFormatter() | |
dateFormatter.dateFormat = "HH:mm" | |
titleStack.addSpacer(10) | |
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) | |
} |
Author
brenoprata10
commented
Aug 19, 2021
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment