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) }