diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2e12f6e..7ad9207 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,10 +1,8 @@ name: Deploy on: - push: - branches: - - main schedule: - cron: "0 4,16 * * *" + workflow_dispatch: jobs: build: runs-on: ubuntu-latest @@ -40,9 +38,29 @@ jobs: path: workers-site/node_modules key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('workers-site/package-lock.json') }} - run: npm i # 执行 Blogroll 的依赖安装 - - run: npm run gen # 相当于 node index.js,生成 opml.xml,opml.json 和 data.json + - run: npm run update # 相当于 node update_files.js,从 Seatable 更新数据到 README.md env: SEATABLE_API_TOKEN: ${{ secrets.SEATABLE_API_TOKEN }} + - name: Commit and push if README.md changed + env: + DEPLOY_REPO: git@github.com:nju-lug/blogroll.git + DEPLOY_BRANCH: main + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + if [ -n "$(git status --porcelain README.md)" ]; then + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add README.md + git commit -m "update README.md from github actions" + mkdir -p ~/.ssh + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan github.com >> ~/.ssh/known_hosts + git push $DEPLOY_REPO HEAD:$DEPLOY_BRANCH + else + echo "No changes detected" + fi + - run: npm run gen # 相当于 node index.js,生成 opml.xml,opml.json 和 data.json - run: npm run build - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 diff --git a/README.md b/README.md index d9e7cf0..92941cb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 欢迎在线浏览:https://blogroll.njulug.org/ -聚合页面使用 Vue 框架编写,在每次 Push 之后,与每天定时 0 点和 12 点的时候,均会通过 GitHub Action 自动集成和部署到 Cloudflare 上。 +聚合页面使用 Vue 框架编写,每天定时 0 点和 12 点,会通过 GitHub Action 自动集成和部署到 Cloudflare 上。 聚合页面由 [@OrangeX4](https://github.com/OrangeX4) 维护,如发现页面上有任何 Bug,欢迎在本 Repo 中提出 Issues。 @@ -36,38 +36,18 @@ https://t.me/NJULUG_Blogroll 提 Pull Request 将其删除,同时我们也会通过 Github Action 的自动更新中的 Log 来判断是否失效。 -## 添加方式 +## 添加/编辑方式 -先 Fork 这个项目,编辑这个 `README.md` 文件的内容,在 **表格最下面一行** 添加(也即按时间顺序),最后提交 Pull Request 进行更改。 +填写表单:[https://table.nju.edu.cn/dtable/forms/b7e232c1-b52b-43ad-8058-3400594cba5a/](https://table.nju.edu.cn/dtable/forms/b7e232c1-b52b-43ad-8058-3400594cba5a/) -如果无 RSS 源,可以使用 `---` 代替,聚合页面将不会抓取。 - -Pull Request 规范:标题为自己的名字,内容可以是对自己和博客的介绍。 - -> 南大协同表格支持建设中... +编辑表单:[https://table.nju.edu.cn/dtable/collection-tables/36161685-5d74-4d48-928f-b6b40174da28](https://table.nju.edu.cn/dtable/collection-tables/36161685-5d74-4d48-928f-b6b40174da28)。如果是之前在README中填写的,可重新[填写表单](https://table.nju.edu.cn/dtable/forms/b7e232c1-b52b-43ad-8058-3400594cba5a/),保证`Name`字段一致即可。 +如果无 RSS 源,可以使用 `---` 代替,聚合页面将不会抓取,仅展示HTML链接。 ## Lists | Name | RSS | HTML | | -- | -- | -- | -| OrangeX4's Blog | https://blog.orangex4.workers.dev/atom.xml | https://blog.orangex4.workers.dev/ | -| Idealclover's Blog | https://idealclover.top/feed | https://idealclover.top/ | -| Cmj's Blog | https://blog.caomingjun.com/atom.xml | https://blog.caomingjun.com/ | -| Mexii's Blog | https://blog.mexii.dev/atom.xml | https://blog.mexii.dev/ | -| LadderOperator's Blog | https://ladderoperator.top/index.xml | https://ladderoperator.top | -| Antares's Blog | https://chr.fan/feed | https://chr.fan | -| lyc8503's Blog | https://blog.lyc8503.net/atom.xml | https://blog.lyc8503.net/ | -| YeungYeah 的乱写地 | https://scottyeung.top/atom.xml | https://scottyeung.top/ | -| yaoge123's Blog | https://www.yaoge123.com/blog/feed | https://www.yaoge123.com/ | -| 南雍随笔 | https://ydjsir.com.cn/atom.xml | https://ydjsir.com.cn/ | -| Kevinpro's Blog | --- | https://www.yuque.com/kevinpro | -| Domon | https://www.domon.cn/rss/ | https://www.domon.cn | -| 极东魔术昼寝结社 | https://blog.jaoushingan.com/atom.xml | https://blog.jaoushingan.com | -| Chivalric Gong | --- | https://gmy-acoustics.github.io/ | -| Persvadisto's Blog | https://persvadisto.github.io/atom.xml | https://persvadisto.github.io/ | -| Yukino's Blog | https://02hyc.github.io/Blog/atom.xml | https://02hyc.github.io/Blog/ | -| Do1e | https://www.do1e.cn/feed | https://www.do1e.cn | ## OPML diff --git a/index.js b/index.js index 86c0c41..a414514 100644 --- a/index.js +++ b/index.js @@ -5,8 +5,6 @@ const Parser = require('rss-parser'); const parser = new Parser(); // 引入 RSS 生成器 const RSS = require('rss'); -// 引入 SeaTableAPI -const { Base } = require('seatable-api'); // 相关配置 const opmlXmlContentTitle = 'NJU-LUG Blogroll'; @@ -18,12 +16,12 @@ const opmlXmlPath = './web/public/opml.xml'; const rssXmlPath = './web/public/rss.xml'; const opmlXmlContentOp = '\n \n ' + opmlXmlContentTitle + '\n \n \n\n'; const opmlXmlContentEd = '\n \n'; -const seatableToken = process.env.SEATABLE_API_TOKEN; + // 解析 README 中的表格,转为 JSON -const pattern = /\| *([^\|]*) *\| *(http[^\|]*) *\| *(http[^\|]*) *\|/g; +const pattern = /\| *([^\|]*) *\| *(http[^\|]*|---) *\| *(http[^\|]*|---) *\|/g; const readmeMdContent = fs.readFileSync(readmeMdPath, { encoding: 'utf-8' }); // 生成 opml.json -let opmlJson = []; +const opmlJson = []; let resultArray; while ((resultArray = pattern.exec(readmeMdContent)) !== null) { opmlJson.push({ @@ -32,118 +30,90 @@ while ((resultArray = pattern.exec(readmeMdContent)) !== null) { htmlUrl: resultArray[3].trim() }); } - -// 解析 SeaTable 中的表格,转为 JSON -async function parseSeaTableToJson(opmlJson) { - const seatableBase = new Base({ - server: "https://table.nju.edu.cn", - APIToken: seatableToken - }); - await seatableBase.auth(); - const tables = await seatableBase.getTables(); - const rows = await seatableBase.listRows(tables[0]['name']); - rows.forEach(row => { - if (row['Name'] && (!opmlJson.some(item => item.title === row['Name'])) && row['RSS'].startsWith('http') && row['HTML'].startsWith('http')) { - opmlJson.push({ - title: row['Name'], - xmlUrl: row['RSS'], - htmlUrl: row['HTML'] - }); - } - }); - return opmlJson; -} +// 保存 opml.json 和 opml.xml +fs.writeFileSync(opmlJsonPath, JSON.stringify(opmlJson, null, 2), { encoding: 'utf-8' }); +const opmlXmlContent = opmlXmlContentOp + + opmlJson.map((lineJson) => ` \n`).join('') + + opmlXmlContentEd; +fs.writeFileSync(opmlXmlPath, opmlXmlContent, { encoding: 'utf-8' }); + +// 异步处理 (async () => { - // 如果定义 SeaTable Token,则将 SeaTable 中的数据合并到 opmlJson 中 - if (seatableToken !== undefined && seatableToken !== '' && seatableToken !== null) { - try { - opmlJson = await parseSeaTableToJson(opmlJson); - } catch (err) { - console.log(err); - } - } - // 保存 opml.json 和 opml.xml - fs.writeFileSync(opmlJsonPath, JSON.stringify(opmlJson, null, 2), { encoding: 'utf-8' }); - const opmlXmlContent = opmlXmlContentOp - + opmlJson.map((lineJson) => ` \n`).join('') - + opmlXmlContentEd; - fs.writeFileSync(opmlXmlPath, opmlXmlContent, { encoding: 'utf-8' }); + // 用于存储各项数据 + const dataJson = []; - // 异步处理 - (async () => { + for (const lineJson of opmlJson) { - // 用于存储各项数据 - const dataJson = []; - - for (const lineJson of opmlJson) { - - try { - - // 读取 RSS 的具体内容 - const feed = await parser.parseURL(lineJson.xmlUrl); - - // 数组合并 - dataJson.push.apply(dataJson, feed.items.filter((item) => item.title && item.content && item.pubDate).map((item) => { - const pubDate = new Date(item.pubDate); - return { - name: lineJson.title, - xmlUrl: lineJson.xmlUrl, - htmlUrl: lineJson.htmlUrl, - title: item.title, - link: item.link, - summary: item.summary ? item.summary : item.content, - pubDate: pubDate, - pubDateYYMMDD: pubDate.toISOString().split('T')[0] - } - })); - - } catch (err) { - - // 网络超时,进行 Log 报告 - console.log(err); - console.log("-------------------------"); - console.log("xmlUrl: " + lineJson.xmlUrl); - console.log("-------------------------"); + try { + // 读取 RSS 的具体内容 + if (!lineJson.xmlUrl.startsWith('http')) { + continue; } + const feed = await parser.parseURL(lineJson.xmlUrl); + + // 数组合并 + dataJson.push.apply(dataJson, feed.items.filter((item) => item.title && item.content && item.pubDate).map((item) => { + const pubDate = new Date(item.pubDate); + return { + name: lineJson.title, + xmlUrl: lineJson.xmlUrl, + htmlUrl: lineJson.htmlUrl, + title: item.title, + link: item.link, + summary: item.summary ? item.summary : item.content, + pubDate: pubDate, + pubDateYYMMDD: pubDate.toISOString().split('T')[0] + } + })); + + } catch (err) { + + // 网络超时,进行 Log 报告 + console.log(err); + console.log("-------------------------"); + console.log("xmlUrl: " + lineJson.xmlUrl); + console.log("-------------------------"); + } + } + + // 按时间顺序排序 + dataJson.sort((itemA, itemB) => itemA.pubDate < itemB.pubDate ? 1 : -1); + // 默认为保存前 n 项的数据, 并保证不超过当前时间 + const curDate = new Date(); + const dataJsonSliced = dataJson.filter((item) => item.pubDate <= curDate).slice(0, Math.min(maxDataJsonItemsNumber, dataJson.length)); + fs.writeFileSync(dataJsonPath, JSON.stringify(dataJsonSliced, null, 2), { encoding: 'utf-8' }); + + // 生成 RSS 文件 + var feed = new RSS({ + title: 'NJU-LUG Blogroll', + description: '南京大学 Linux User Group 收集同学和校友们的 Blog', + feed_url: 'https://blogroll.njulug.org/rss.xml', + site_url: 'https://blogroll.njulug.org/', + image_url: 'https://blogroll.njulug.org/assets/logo.56c0d74c.png', + docs: 'https://blogroll.njulug.org/', + managingEditor: 'NJU-LUG', + webMaster: 'NJU-LUG', + copyright: '2022 NJU-LUG', + language: 'cn', + pubDate: dataJson[0].pubDate, + ttl: '60', + }); - // 按时间顺序排序 - dataJson.sort((itemA, itemB) => itemA.pubDate < itemB.pubDate ? 1 : -1); - // 默认为保存前 n 项的数据, 并保证不超过当前时间 - const curDate = new Date(); - const dataJsonSliced = dataJson.filter((item) => item.pubDate <= curDate).slice(0, Math.min(maxDataJsonItemsNumber, dataJson.length)); - fs.writeFileSync(dataJsonPath, JSON.stringify(dataJsonSliced, null, 2), { encoding: 'utf-8' }); - - // 生成 RSS 文件 - var feed = new RSS({ - title: 'NJU-LUG Blogroll', - description: '南京大学 Linux User Group 收集同学和校友们的 Blog', - feed_url: 'https://blogroll.njulug.org/rss.xml', - site_url: 'https://blogroll.njulug.org/', - image_url: 'https://blogroll.njulug.org/assets/logo.56c0d74c.png', - docs: 'https://blogroll.njulug.org/', - managingEditor: 'NJU-LUG', - webMaster: 'NJU-LUG', - copyright: '2022 NJU-LUG', - language: 'cn', - pubDate: dataJson[0].pubDate, - ttl: '60', + for (let item of dataJsonSliced) { + feed.item({ + title: item.title, + description: item.summary, + url: item.link, // link to the item + author: item.name, // optional - defaults to feed author property + date: item.pubDate.toISOString(), // any format that js Date can parse. }); + } - for (let item of dataJsonSliced) { - feed.item({ - title: item.title, - description: item.summary, - url: item.link, // link to the item - author: item.name, // optional - defaults to feed author property - date: item.pubDate.toISOString(), // any format that js Date can parse. - }); - } + // 保存 rss.xml 文件 + const rssXmlContent = feed.xml(); + fs.writeFileSync(rssXmlPath, rssXmlContent, { encoding: 'utf-8' }); - // 保存 rss.xml 文件 - const rssXmlContent = feed.xml(); - fs.writeFileSync(rssXmlPath, rssXmlContent, { encoding: 'utf-8' }); - })(); })(); diff --git a/package-lock.json b/package-lock.json index 4380ad6..0f760ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blogroll", - "version": "0.0.1", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "blogroll", - "version": "0.0.1", + "version": "2.0.0", "dependencies": { "rss": "^1.2.2", "rss-parser": "^3.12.0", diff --git a/package.json b/package.json index aa16a42..5db8a70 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "blogroll", - "version": "1.0.0", + "version": "2.0.0", "scripts": { "dev": "cd web && vite", + "update": "node update_files.js", "gen": "node index.js", "build": "cd web && vite build", "preview": "cd web && vite preview --port 5050" diff --git a/update_files.js b/update_files.js new file mode 100644 index 0000000..aa34e4d --- /dev/null +++ b/update_files.js @@ -0,0 +1,57 @@ +// 文件读取包 +const fs = require('fs'); +// 引入 SeaTableAPI +const { Base } = require('seatable-api'); +// exit函数 +const { exit } = require('process'); + +// 相关配置 +const seatableToken = process.env.SEATABLE_API_TOKEN; +const readmeMdPath = './README.md'; +// 读取 README.md +const readmeMdContent = fs.readFileSync(readmeMdPath, { encoding: 'utf-8' }); + +// 解析 SeaTable 中的表格,转为 JSON +async function parseSeaTableToJson() { + const seatableBase = new Base({ + server: "https://table.nju.edu.cn", + APIToken: seatableToken + }); + try { + await seatableBase.auth(); +} catch (err) { + console.log('Seatable API Token 无效,请检查环境变量 SEATABLE_API_TOKEN 是否正确设置。'); + exit(1); + } + const tables = await seatableBase.getTables(); + const rows = await seatableBase.listRows(tables[0]['name']); + const rows_reverse = rows.reverse(); + var opmlJson = []; + rows_reverse.forEach(row => { + // 根据Name去重并保留最后一个 + if (row['Name'] && !(opmlJson.find((item) => item.title === row['Name']))) { + if (opmlJson.find((item) => item.htmlUrl === row['HTML'])) { + return; + } + opmlJson.push({ + title: row['Name'], + xmlUrl: row['RSS'], + htmlUrl: row['HTML'] + }); + } + }); + return opmlJson.reverse(); +} + + +(async () => { + // 从 SeaTable 中读取数据 + const opmlJson = await parseSeaTableToJson(); + + // 更新 README.md 中的表格内容 + const tableStart = readmeMdContent.indexOf('| -- | -- | -- |') + 22; + const tableEnd = readmeMdContent.indexOf('## OPML') - 2; + const tableContent = opmlJson.map((lineJson) => `| ${lineJson.title} | ${lineJson.xmlUrl} | ${lineJson.htmlUrl} |`).join('\n') + '\n'; + const newReadmeMdContent = readmeMdContent.slice(0, tableStart) + tableContent + readmeMdContent.slice(tableEnd); + fs.writeFileSync(readmeMdPath, newReadmeMdContent, { encoding: 'utf-8' }); +})();