Skip to content

Instantly share code, notes, and snippets.

@walkure
Last active April 30, 2026 13:07
Show Gist options
  • Select an option

  • Save walkure/cf3b112b4705bdfffdec9cedee6d6e4d to your computer and use it in GitHub Desktop.

Select an option

Save walkure/cf3b112b4705bdfffdec9cedee6d6e4d to your computer and use it in GitHub Desktop.
YoutubeのRSS Feedが返す404をfeed readerに無視させるCloudflare Worker
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// 1. クエリパラメータまたはパスから channel_id を取得
// 例: https://worker-name.subdomain.workers.dev/?channel_id=UC...
// 例: https://worker-name.subdomain.workers.dev/UC...
let channelId = url.searchParams.get("channel_id") || url.pathname.split("/")[1];
const channelIdRegex = /^UC[a-zA-Z0-9_-]{22}$/;
if (!channelId || !channelIdRegex.test(channelId)) {
return new Response("Invalid Channel ID format.", { status: 400 });
}
const kvKey = `rss_data_${channelId}`;
const now = Date.now();
// 1. KVからキャッシュデータを取得
const cached = await env.RSS_CACHE.get(kvKey, { type: "json" });
// 2. キャッシュがあり、かつ1時間(3600000ms)以内ならそれを即レス
if (cached && (now < cached.expiresAt)) {
// console.log(`returns cached data ${channelId}`)
return new Response(cached.xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"X-Proxy-Cache": "HIT"
}
});
}
// 3. キャッシュがない、または1時間過ぎた場合はYouTubeへ
const targetUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
try {
const response = await fetch(targetUrl, {
headers: { "User-Agent": "Mozilla/5.0" }
});
if (response.ok) {
let xmlText = await response.text();
// Shorts除去
const newXml = filterShorts(xmlText)
// KVを更新(1時間後のタイムスタンプを付与)
const nextExpires = now + 3600000;
ctx.waitUntil(
env.RSS_CACHE.put(kvKey, JSON.stringify({
xml: newXml,
expiresAt: nextExpires
}), { expirationTtl: 604800 }) // バックアップとして1週間保持
);
// console.log(`returns fresh data and stored ${channelId}`)
return new Response(newXml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"X-Proxy-Cache": "MISS"
}
});
}
// 4. YouTubeが404/403等を返した場合、期限切れでもKVにある古いデータを返す
if (cached) {
// console.log(`Upstream error ${response.status}. Using expired cache for ${channelId}`);
return new Response(cached.xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"X-Cache-Status": "stale-from-kv"
}
});
}
return new Response(`Upstream Error. cache not found for ${channelId}`, { status: 429 });
} catch (e) {
if (cached) return new Response(cached.xml, { headers: { "Content-Type": "application/xml" } });
return new Response("Network Error", { status: 429 });
}
}
};
function filterShorts(xml) {
// 1. まず、全体の閉じタグ </feed> を一旦切り離して、最後を綺麗にする
const cleanXml = xml.trim();
const feedCloseTag = "</feed>";
// 閉じタグより前の部分だけを取り出す
const bodyBeforeClose = cleanXml.endsWith(feedCloseTag)
? cleanXml.slice(0, -feedCloseTag.length)
: cleanXml;
// 2. <entry> で分割
const parts = bodyBeforeClose.split("<entry>");
const header = parts.shift(); // <feed> ... <author> などのヘッダー
// 3. 各エントリをフィルタリング
const filteredEntries = parts.filter(entry => {
// リンクに shorts が含まれていたら除外
return !entry.includes('href="https://www.youtube.com/shorts/');
});
// 4. 再結合
// ヘッダー + フィルタ後のエントリ群 + 最後に必ず </feed>
let finalXml = header;
if (filteredEntries.length > 0) {
finalXml += filteredEntries.map(e => "<entry>" + e).join("");
}
// 最後に確実に閉じタグを付与
finalXml += "\n" + feedCloseTag;
return finalXml;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment