Last active
April 30, 2026 13:07
-
-
Save walkure/cf3b112b4705bdfffdec9cedee6d6e4d to your computer and use it in GitHub Desktop.
YoutubeのRSS Feedが返す404をfeed readerに無視させるCloudflare Worker
This file contains hidden or 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
| 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