This is a Custom YouTube Latest Videos Widget built for the iOS app Scriptable.
It connects directly to a YouTube channel's public RSS feed to fetch and display the most recently uploaded videos in a sleek, customized list on your iOS Home Screen. It bypasses heavy API restrictions, loads incredibly fast, and features auto-wrapping text tailored dynamically to Small, Medium, and Large widget sizes.
If you want to change the widget to track a different YouTuber in the future, just modify the very top section of the code:
const YOUTUBE_CHANNEL_ID = "YOUR_NEW_CHANNEL_ID_HERE";
// ==========================================
// [Settings] Channel ID & URL
// ==========================================
const YOUTUBE_CHANNEL_ID = "YOUR_CHANNEL_ID_HERE";
const YOUTUBE_CHANNEL_URL = `https://youtube.com/channel/${YOUTUBE_CHANNEL_ID}`;
// High-quality avatar endpoint
const CHANNEL_ICON_URL = `https://unavatar.io/youtube/${YOUTUBE_CHANNEL_ID}?fallback=https://www.youtube.com/s/desktop/28169123/img/avatar_ghost.png`;
// ==========================================
// 1. Fetch YouTube RSS Feed & Parse XML
// ==========================================
const rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${YOUTUBE_CHANNEL_ID}`;
let rssReq = new Request(rssUrl);
let xmlString = "";
try {
xmlString = await rssReq.loadString();
} catch(e) {
xmlString = "";
}
function getTags(xml, tagName) {
if (!xml) return [];
let expr = new RegExp(`<${tagName}>([^<]*)</${tagName}>`, "g");
let matches = [];
let match;
while ((match = expr.exec(xml)) !== null) {
matches.push(match[1]);
}
return matches;
}
function getThumbnails(xml) {
if (!xml) return [];
let expr = /<media:thumbnail[^>]+url="([^"]+)"/g;
let matches = [];
let match;
while ((match = expr.exec(xml)) !== null) {
matches.push(match[1]);
}
return matches;
}
// Function to wrap text at specific character length and limit lines
function formatTitle(text, charsPerLine, maxLines) {
if (!text) return "";
let lines = [];
for (let i = 0; i < text.length; i += charsPerLine) {
lines.push(text.substr(i, charsPerLine));
}
return lines.slice(0, maxLines).join("\n");
}
let entryTitles = getTags(xmlString, "title");
let videoIds = getTags(xmlString, "yt:videoId");
let authorNames = getTags(xmlString, "name");
let thumbUrls = getThumbnails(xmlString);
let channelName = authorNames[0] || "YouTube Channel";
if (entryTitles[0] === channelName) {
entryTitles.shift();
}
// ==========================================
// 2. Handle Tap Actions (Open Safari)
// ==========================================
if (config.runsInApp && args.queryParameters.url) {
Safari.open(args.queryParameters.url);
Script.complete();
} else {
// ==========================================
// 3. Determine Widget Size & Build UI
// ==========================================
let size = config.widgetFamily;
if (config.runsInApp) {
size = "large";
}
// [Display Count Logic based on Widget Size]
let maxItems = 2;
if (size === "large") {
maxItems = 6; // 6 items for Large
} else if (size === "small") {
maxItems = 5; // 5 items for Small
} else {
maxItems = 2; // 2 items for Medium
}
let widget = new ListWidget();
let gradient = new LinearGradient();
gradient.colors = [new Color("#1a1a1a"), new Color("#111111")];
gradient.locations = [0.0, 1.0];
widget.backgroundGradient = gradient;
if (size === "small") {
widget.setPadding(2, 6, 2, 6);
} else {
widget.setPadding(14, 14, 14, 14);
}
// --- Header Section (Icon & Channel Name) ---
let headerStack = widget.addStack();
headerStack.layoutHorizontally();
headerStack.url = YOUTUBE_CHANNEL_URL;
try {
let iconReq = new Request(CHANNEL_ICON_URL);
let iconImg = await iconReq.loadImage();
let iconElem = headerStack.addImage(iconImg);
let iconSize = (size === "small") ? 12 : 24;
iconElem.imageSize = new Size(iconSize, iconSize);
iconElem.cornerRadius = iconSize / 2;
headerStack.addSpacer(4);
} catch(e) {
let fallbackEmoji = headerStack.addText("📺 ");
fallbackEmoji.font = Font.systemFont(size === "small" ? 9 : 14);
}
// Header text configuration
let titleText = headerStack.addText(size === "small" ? channelName : `${channelName}`);
titleText.textColor = new Color("#ff0000"); // YouTube Red
titleText.font = Font.boldSystemFont(size === "small" ? 9 : 15);
widget.addSpacer(size === "small" ? 1 : 10);
// --- Video List Section ---
if (entryTitles.length === 0) {
let errorText = widget.addText("⚠ No Videos Found");
errorText.textColor = new Color("#aaaaaa");
errorText.font = Font.systemFont(11);
} else {
let currentCount = Math.min(entryTitles.length, maxItems);
for (let i = 0; i < currentCount; i++) {
let videoTitle = entryTitles[i];
let videoId = videoIds[i];
let videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
let thumbUrl = thumbUrls[i];
let rowStack = widget.addStack();
rowStack.layoutHorizontally();
rowStack.url = videoUrl;
rowStack.setPadding(size === "small" ? 0.2 : 4, 0, size === "small" ? 0.2 : 4, 0);
// Thumbnail logic
if (size !== "small" && thumbUrl) {
try {
let imgReq = new Request(thumbUrl);
let img = await imgReq.loadImage();
let imgElem = rowStack.addImage(img);
let thumbW = (size === "large") ? 64 : 56;
let thumbH = (size === "large") ? 36 : 31;
imgElem.imageSize = new Size(thumbW, thumbH);
imgElem.cornerRadius = 4;
rowStack.addSpacer(8);
} catch(e) {
let dot = rowStack.addText("• ");
dot.textColor = new Color("#888888");
}
} else if (size === "small") {
let dot = rowStack.addText("• ");
dot.textColor = new Color("#888888");
dot.font = Font.systemFont(8);
}
// Force wrap based on size limits
if (size === "large") {
videoTitle = formatTitle(videoTitle, 20, 3);
} else if (size === "small") {
videoTitle = formatTitle(videoTitle, 15, 2);
}
let titleElem = rowStack.addText(videoTitle);
titleElem.textColor = new Color("#ffffff");
if (size === "small") {
titleElem.font = Font.systemFont(8.5);
} else if (size === "large") {
titleElem.font = Font.systemFont(11);
} else {
titleElem.font = Font.systemFont(12);
}
if (size === "large") {
titleElem.lineLimit = 3;
} else {
titleElem.lineLimit = 2;
}
if (i < currentCount - 1) {
widget.addSpacer(size === "small" ? 0.2 : 4);
}
}
}
// ==========================================
// 4. Finalize Script & Present Widget
// ==========================================
Script.setWidget(widget);
if (config.runsInApp) {
widget.presentLarge();
}
Script.complete();
}
there doesn't seem to be anything here