如何订阅 Q 外的 RSS?方法来了

如何订阅 Q 外的 RSS?方法来了

January 06, 2024
工具箱 , 学习 ,

起因

前文有写到关于 RSS 订阅的一些事情,想了解的可以看看 打破信息茧房及一款 RSS 阅读器推荐 一文。

不过这两天在实际使用过程中发现有部分网站如:V2ex椒盐豆豉 等网站的订阅在通过 yarr 添加的时候会提示 No feeds found at the given url. 的提醒信息。

当时我还以为是 yarr 对这些网站的 xml 格式支持不全导致,所以跑去研究起了 yarr 关于解析 xml 文件部分的源代码,在尝试将 2.4 的部分代码和原作者最新的部分合并后我在本地测试发现似乎可以正常添加,所以重新 build 了一个镜像发布到 docker 上。

不过,奇怪的事情发生了。

我之前在本地测试明明可以,但是在自己的 VPS 中即便我重新拉取镜像、重新创建容器,却依旧提示上面的错误。这就让我有点「满头大汉」了,本来差点又要跑去研究源代码,不过忽然灵光一闪,想到这两个网站似乎都有一个共性 ——「在墙外」,而我的服务器又在国内,正常情况下自然是无法访问的,之前在本地测试通是因为我的路由器上有小猫做分流,所以自然能正常访问。

至此算是破案了,不过很想吐槽一句:yarr 的错误信息提示真的很有问题,这种情况不是应该提示目标网站访问超时吗?

当时我能想到的只有一种方式:在服务器上装个小猫,让流量走代理就可以了,不过被我直接 PASS 了,因为环境的关系我并不想在这些大厂的服务器上安装这种东西。

CF Worker 转发 Rss

而后想到之前 Memos 使用 tgbot 机器人时通过 CF Worker 转发请求的折腾。那有没有可能也可以通过 CF Worker 转发 RSS 的呢?答案自然是有的,通过一些搜索,我在 蜜柑计划 RSS 无法访问的解决办法 一贴中找到了解决办法。

因为没有很复杂的功能,所以文内提到的代码几乎开箱即用,可以说有手就行。

在 CF 中新建一个 Worker,并将下方的示例代码贴入新 Workers 中,并将代码中 const yourDomain = 'your.workers.dev'; 中后面的部分换成你的 Worker 链接 (注意没有 https ),部署即可。

/*
 * https://github.com/netnr/workers
 *
 * 2019-10-12 - 2022-05-05
 * netnr
 *
 * https://github.com/Rongronggg9/rsstt-img-relay
 *
 * 2021-09-13 - 2022-05-29
 * modified by Rongronggg9
 * 
 * 2023-4-21 
 * modified by papersman
 */

export default {
    async fetch(request, _env) {
        return await handleRequest(request);
    }
}

/**
 * Configurations
 */
const yourDomain = 'your.workers.dev';
const config = {
    // 是否丢弃请求中的 Referer,在目标网站应用防盗链时有用
    dropReferer: true,
};

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
    //请求头部、返回对象
    let reqHeaders = new Headers(request.headers),
        outBody, outStatus = 200, outStatusText = 'OK', outCt = null, outHeaders = new Headers({
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
            "Access-Control-Allow-Headers": reqHeaders.get('Access-Control-Allow-Headers') || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token"
        });

    try {
        //取域名第一个斜杠后的所有信息为代理链接
        let url = request.url.substr(8);
        url = decodeURIComponent(url.substr(url.indexOf('/') + 1));

        //需要忽略的代理
        if (request.method == "OPTIONS" || url.length < 3 || url.indexOf('.') == -1 || url == "favicon.ico" || url == "robots.txt") {
            //输出提示
            const invalid = !(request.method == "OPTIONS" || url.length === 0)
            outBody = JSON.stringify({
                code: invalid ? 400 : 0,
                usage: 'https://'+yourDomain+'/https://mikanani.me/...',
                source: '将 your.workers.dev 换成自己的workers的地址.使用的时候, RSS地址位置填入 '+'https://'+yourDomain+'/https://mikanani.me/RSS/...'
            });
            outCt = "application/json";
            outStatus = invalid ? 400 : 200;
        } else {
            url = fixUrl(url);

            //构建 fetch 参数
            let fp = {
                method: request.method,
                headers: {}
            }
            // 发起 fetch
            let fr = (await fetch(url, fp));
            outCt = fr.headers.get('content-type');

            //保留头部其它信息
            const dropHeaders = ['content-length', 'content-type', 'host'];
            if (config.dropReferer) dropHeaders.push('referer');
            let he = reqHeaders.entries();
            for (let h of he) {
                const key = h[0], value = h[1];
                if (!dropHeaders.includes(key)) {
                    fp.headers[key] = value;
                }
            }
            if (config.dropReferer && url.includes('.sinaimg.cn/')) fp.headers['referer'] = 'https://weibo.com/';

            // 当访问mikanani.me/RSS的时候,将返回的xml中的mikanani.me替换
            if (url.includes('mikanani.me/RSS')) {
                const response = await fetch(url, fp);
                const text = await response.text();
                outBody = text.replace(/mikanani.me\/Download\//g, yourDomain+'/https://mikanani.me/Download/');
                outCt = response.headers.get('content-type');
                outStatus = response.status;
                outStatusText = response.statusText;
            } else if (url.includes('acg.rip/.xml')) {  //当访问acg.rip/.xml的时候,将返回的xml中的acg.rip/t/替换
                const response = await fetch(url, fp);
                const text = await response.text();
                outBody = text.replace(/acg.rip\/t\//g, yourDomain+'/https://acg.rip/t/');
                outCt = response.headers.get('content-type');
                outStatus = response.status;
                outStatusText = response.statusText;
            } else if (url.includes('bangumi.moe/rss')) {  //当访问bangumi.moe/rss的时候,将返回的xml中的bangumi.moe/download替换
                const response = await fetch(url, fp);
                const text = await response.text();
                outBody = text.replace(/bangumi.moe\/download\//g, yourDomain+'/https://bangumi.moe/download/');
                outCt = response.headers.get('content-type');
                outStatus = response.status;
                outStatusText = response.statusText;
            } else {
                outBody = fr.body;
                outStatus = fr.status;
                outStatusText = fr.statusText;
            };

            if (["POST", "PUT", "PATCH", "DELETE"].indexOf(request.method) >= 0) {
                const ct = (reqHeaders.get('content-type') || "").toLowerCase();
                fp.headers['content-type'] = ct
                if (ct.includes('application/json')) {
                    fp.body = JSON.stringify(await request.json());
                } else if (ct.includes('application/text') || ct.includes('text/html')) {
                    fp.body = await request.text();
                } else if (ct.includes('form')) {
                    fp.body = await request.formData();
                } else {
                    fp.body = await request.blob();
                }
            };
        }
    } catch (err) {
        outBody = err.stack;
        outCt = "text/plain;charset=UTF-8";
        outStatus = 500;
        outStatusText = "Internal Server Error";
    }

    //设置类型
    if (outCt && outCt != "") {
        outHeaders.set("content-type", outCt);
    }
    
    let response = new Response(outBody, {
        status: outStatus,
        statusText: outStatusText,
        headers: outHeaders
    })

    return response;
}

/**
 * Fix URL
 * @param {string} url 
 */
function fixUrl(url) {
    if (url.startsWith('http://') || url.startsWith('https://')) {
        return url;
    } else {
        return 'https://' + url;
    }
}

使用方法也很简单,在你的 worker 地址后面跟上需要订阅的 rss 即可,这里以订阅 v2ex 为例:

https://your.worker.dev/https://www.v2ex.com/index.xml

加入评论