豆瓣书影音同步 GitHub Action

2022-03-14
6分钟阅读时长

提醒:本文最后更新于 740 天前,文中所描述的信息可能已发生改变,请谨慎使用。

Featured Image

2023-07-12 更新:《关于豆瓣图片无法直接使用的说明》

简介

doumark-action 是我前段时间造的一个轮子。它是一款 GitHub Action,支持在 GitHub 中同步你的豆瓣书影音数据到本地的文件或者 Notion 中。我利用它,定时同步我的豆瓣观影数据到我的博客仓库中,并利用 Hugo 读取文件数据渲染成页面,观影 是最终的效果。

使用

使用其实很简单,在你的博客仓库中新建 .github/workflows/douban.yml 文件,以观影为例添加如下内容。它实现了每小时自动抓取你的豆瓣观影记录并更新到文件中,如果发现文件有更新则触发 commit 提交。

name: douban
on: 
  schedule:
  - cron: "30 * * * *"

jobs:
  douban:
    name: Douban mark data sync
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: movie
      uses: lizheming/doumark-action@master
      with:
        id: lizheming
        type: movie
        format: csv
        dir: ./douban

    - name: Commit
      uses: EndBug/add-and-commit@v8
      with:
        message: 'chore: update douban data'
        add: './douban'

该 workflow 总共分为三步,第一步初始化 Git 仓库;第二步调用 doumark-action 同步豆瓣账号 lizhemingmovie 类型数据到 ./douban 文件夹下,并保存为 csv 格式文件;最后一步则是当 ./douban 文件夹下有更新则调用插件提交修改。

Notion

如果是要同步到 Notion 中会稍微复杂一点。需要先准备好 Notion Token 并初始化好页面。

  1. 我们可以在 My Integrations 里创建机器人得到 NOTION_TOKEN
  2. 电影 | 阅读 | 音乐 基于这三个模板点击右上角的 Duplicate 按钮渲染复制页面。
  3. 复制后的页面右上角选择右上角的 Share - Invite 将第一步创建的机器人加入,这样机器人就有权限更新你的页面数据。
# .github/workflows/douban.yml
name: douban
on: 
  schedule:
  - cron: "30 * * * *"

jobs:
  douban:
    name: Douban mark data sync
    runs-on: ubuntu-latest
    steps:
    - name: movie
      uses: lizheming/doumark-action@master
      with:
        id: lizheming
        type: movie
        format: notion
        dir: xxxx
        notion_token: ${{ secrets.notion_token }}

其中 format 需要为 notiondir 为 Notion 页面 ID,Notion 页面 URL 第一个随机字符即为页面的 ID。

渲染

数据已经有了,剩下的就是我们需要读取该数据源的数据,并渲染出页面。除了数据渲染之外,我还给自己增加了筛选查找的需求,所以我在头部还渲染了一些筛选项。

{{$movies := getCSV "," "douban/movie.csv" }}
{{$scratch := newScratch}}
{{$scratch.Add "genres" slice}}
{{range $idx, $movie := $movies}}
  {{if ne $idx 0}}
    {{$scratch.Set "genres" (union ($scratch.Get "genres") (split (index $movie 7) ","))}}
  {{end}}
{{end}}
<div class="sc-ksluID gFnzgG">
  <!--分类筛选-->
  <div class="sc-bdnxRM jvCTkj">
    <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="genres" data-method="contain" data-value="">全部</a>
    {{range $genre := $scratch.Get "genres"}}
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="genres" data-method="contain" data-value="{{$genre}}">{{$genre}}</a>
    {{end}}
  </div>

  
  <!--时间筛选-->
  <div class="sc-bdnxRM jvCTkj">
    <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="year" data-method="equal" data-value="">全部</a>
    {{range $year := (seq 2022 -1 2009)}}
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="year" data-method="equal" data-value="{{$year}}">{{$year}}</a>
    {{end}}
  </div>
  
  <!--评分筛选-->
  <div class="sc-bdnxRM jvCTkj">
    <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="star" data-method="equal" data-value="">全部</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="5">五星</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="4">四星</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="3">三星</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="2">二星</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="1">一星</a>
    <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="0">零星</a>
  </div>

  <!--排序规则-->
  <div class="sc-bdnxRM jvCTkj sort-by">
    <a href="javascript:void 0;" class="sort-by-item active" data-order="time">
      观影时间排序
    </a>
    <a href="javascript:void 0;" class="sort-by-item" data-order="rating">
      网友评分排序
    </a>
  </div>

  <!-影片列表-->
  <div class="sc-dIsUp fIuTG">
    {{range $idx, $movie := $movies}}
    <!--排除第一行表头-->
    {{if ne $idx 0 }}
    <div 
      class="sc-gKAaRy dfdORB" 
      data-year="{{index $movie 9}}" 
      data-star="{{index $movie 8}}"
      data-rating="{{index $movie 6}}"
      data-genres="{{index $movie 7}}"  
    >
      <a href="{{index $movie 5}}" target="_blank">
        <div class="sc-hKFxyN HPRth">
          <div class="lazyload-wrapper ">
            <img class="lazy" data-src="https://dou.img.lithub.cc/movie/{{ index (findRE `\d+` (index $movie 5)) 0 }}.jpg" referrer-policy="no-referrer" loading="lazy" alt="{{index $movie 1}}" width="150" height="220">
          </div>
        </div>
        <div class="sc-iCoGMd kMthTr">{{index $movie 1}}</div>
        <div class="sc-fujyAs eysHZq">
          <span class="sc-jSFjdj jcTaHb">
            {{range $star := (seq 0 2 8)}}
            <svg viewBox="0 0 24 24" width="24" height="24" class="sc-dlnjwi {{if gt (index $movie 6) $star}}lhtmRw{{else}}gaztka{{end}}">
              <path fill="none" d="M0 0h24v24H0z"></path>
              <path fill="currentColor" d="M12 18.26l-7.053 3.948 1.575-7.928L.587 8.792l8.027-.952L12 .5l3.386 7.34 8.027.952-5.935 5.488 1.575 7.928z"></path>
            </svg>
            {{end}}
          </span>
          <span class="sc-pNWdM iibjPt">{{index $movie 6}}</span>
        </div>
      </a>
    </div>
    {{end}}
    {{end}}
  </div>  
</div>

整体的布局我使用了 Flex 布局,增加了图片懒加载。

搜索

使用 CSS 的属性选择器,可以非常简单的实现搜索的功能。事先将数据通过属性挂载在 DIV 上,通过 [data-year^=2022][data-genres*=喜剧] 就可以查询到 2022 年看过的喜剧片了!

function search(e) {
  // 隐藏全部电影
  document.querySelectorAll('.dfdORB').forEach(item => item.classList.add('hide'));
  // 移除当前筛选项之前的选项
  document.querySelector(`.dvtjjf.active[data-search="${e.target.dataset.search}"]`)?.classList.remove('active');
  // 如果选择的是非全部选项,则高亮该选项
  if(e.target.dataset.value) {
    e.target.classList.add('active');
  }

  // 找到所有筛选项的值
  const searchItems = document.querySelectorAll('.dvtjjf.active');
  // 根据筛选值拼接 CSS 选择器,JSON 数据类型的需要使用 *=,其它的需要使用 ^=
  const attributes = Array.from(searchItems, searchItem => {
    const property = `data-${searchItem.dataset.search}`;
    const logic = searchItem.dataset.method === 'contain' ? '*' : '^';
    const value = searchItem.dataset.method === 'contain' ? `${searchItem.dataset.value}` : searchItem.dataset.value;
    return `[${property}${logic}='${value}']`;
  });
  const selector = `.dfdORB${attributes.join('')}`;
  // 找到目标元素对其进行展现操作
  document.querySelectorAll(selector).forEach(item => item.classList.remove('hide'));
}

window.addEventListener('click', function(e) {
  if(e.target.classList.contains('sc-gtsrHT')) {
    e.preventDefault();
    search(e);
  }
});

排序

由于我使用了 Flex 布局,所以排序这个实行实际上是可以通过 Flex 的 order 属性来实现的。这样做的好处就是我不需要真的去修改 DOM 结构,只需要生成或者删除 CSS 就好了。

function sort(e) {
  const sortBy = e.target.dataset.order;
  const style = document.createElement('style');
  style.classList.add('sort-order-style');
  document.querySelector('style.sort-order-style')?.remove();
  document.querySelector('.sort-by-item.active')?.classList.remove('active');
  e.target.classList.add('active');
  if(sortBy === 'rating') {
    const movies = Array.from(document.querySelectorAll('.dfdORB'));
    movies.sort((movieA, movieB) => {
      const ratingA = parseFloat(movieA.dataset.rating) || 0;
      const ratingB = parseFloat(movieB.dataset.rating) || 0;
      if(ratingA === ratingB) {
        return 0;
      }
      return ratingA > ratingB ? -1 : 1;
    });
    const stylesheet = movies.map((movie, idx) => `.dfdORB[data-rating="${movie.dataset.rating}"] { order: ${idx}; }`).join('\r\n');
    style.innerHTML = stylesheet;
    document.body.appendChild(style);
  }
}
window.addEventListener('click', function(e) {
  if(e.target.classList.contains('sort-by-item')) {
    e.preventDefault();
    sort(e);
  }
});

起因

很早以前我就养成了看完电影就要上豆瓣上标记一下的习惯,并在每年年末的时候统计一下。为了满足自己的需求,很早之前我写过一款 Chrome 插件,用于统计豆瓣电影记录,具体可以看这篇文章《豆瓣电影统计插件For Chrome》

在后来无意间知道了牧风老师开发的布克牧为,用户同步豆瓣记录数据并支持在第三方网站中挂件展示。所以我为我的博客增加了观影页面,用来展示我看过的电影。后来,每当我和朋友聊电影,想要推荐之前看过的电影给他们的时候,它也成为了重要的查找入口。

布克牧为的第三方挂件样式很好看,但筛选功能偏弱,仅支持分类的筛选。对于我有搜索和统计的需求其实没办法很好的满足。再加之最近布克牧为时长不出数据,变的不太稳定,导致我又有了重新造轮子的想法。

自从博客切换成 Hugo 之后,我对 SSG(Server Side Generate) 就非常的痴迷,连评论都是使用 SSG 的方式渲染到页面上的,具体可以查看我之前写的这篇文章《静态博客如何高性能插入评论》。于是关于这次的功能理所当然我也想使用类似的方式。

所以最开始我是写了个独立的服务,该服务会定时抓取数据并更新到数据库中,同时提供了 API 用于获取数据。在博客中则去调用该接口获取到数据后渲染页面。后来因为需要找一个第三方定义任务服务,用于定时触发抓取任务接口。更新数据后还需要调用博客的构建触发器,同时又觉得每次构建的时候都需要花时间去请求一次接口有点浪费,就一直在思考有没有其它的方式。

其实 Hugo 除了支持 JSON 接口的数据读取之外,也支持本地 CSV 文件的数据读取。直接读取从库中的表格文件获取到数据能减少不必要的网络请求,而表格文件更新的时候会自动触发 Git 操作从何触发博客的构建任务。所以最终就想到了 GitHub Action 的方案,通过免费的 GitHub Action 触发 CSV 文件的更新,最终触发构建更新。

于是乎「doumark-action」这个项目就诞生了!

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

30 评论

lyd123qw2008 Edge112.0 Windows 10
2023-05-02 17:05:09 回复

多年前用过你的 egg.js,在 issues 区和你交流过,今天因为写博客,对比评论系统,又重遇了。

怡红公子 Mobile Safari16.4 iOS 16.4.1
2023-05-02 17:23:59 回复

@lyd123qw2008: 应该是 ThinkJS 吧 [哭笑不得] 评论系统也是用 ThinkJS 写的,可以试试呀,有问题可以提 issue~

Henry Edge111.0 Mac OS 10.15.7
2023-02-26 09:47:13 回复

用 Action 同步数据到 Notion,这样会不会有封号风险?https://www.v2ex.com/t/817831

怡红公子 Chrome110.0 Mac OS 10.15.7
2023-02-26 10:42:51 回复

@Henry: 我一直使用的是 CSV 存仓库的方式,所以不是特别确定。如果担心这个风险问题,也可以使用 Drone CI 来跑任务 https://plugins.drone.io/plugins/doumark

林木木 Edge109.0 Mac OS 10.15.7
2023-02-11 13:26:21 回复

单条数据也直接调用本地 data 咯,发现书籍有介绍 intro ,电影 intro 没有,能设置抓取不?

怡红公子 Chrome109.0 Mac OS 10.15.7
2023-02-11 13:43:00 回复

@林木木: 电影我之前看是没有 intro 内容的,你可以改成 json 类型存储,可以看到所有的原始接口数据。

林木木 Edge109.0 Mac OS 10.15.7
2023-01-20 15:54:16 回复

是否因为 2023 年的缘故,前端显示有问题

怡红公子 Chrome108.0 Mac OS 10.15.7
2023-01-21 00:49:09 回复

@林木木: 你是说你的阅读页面数据错乱导致展示有问题吧?这个是最近发现的一个 Bug,使用的 CSV 库存储成 CSV 的时候列顺序不是固定的,我之前的模板指定了列数据,所以列错乱导致生成的页面有问题。目前我已经按照文章中模板指定的列顺序强制指定了 CSV 存储的顺序了,现在没有这个问题。你的阅读页面应该是改了模板所以可能不正常,之前我看是你的电影页面不正常所以排查了一下。

Charles Chin Chrome108.0 Windows 10
2022-12-14 09:55:17 回复

有 2 个小问题请教一下,这个保存时,可以同时保存为jsoncsv两种格式吗;还有支持电影和电视剧分开吗?

怡红公子 Chrome108.0 Mac OS 10.15.7
2022-12-14 11:06:46 回复

@Charles Chin: 1. 目前代码是不支持的,需要的话可以先输出 json 然后自己再写个 action 生成 csv,这样改造比较简单
2. 电影和电视剧在接口数据里是可以区分开的,如果需要的话可以自己对 json 数据再处理一下也行

TheWanderingAllison Edge107.0 Windows 10
2022-11-16 07:11:12 回复

感谢工具,已经完成了读过的书影音同步,另外想问下可以用这个抓取想读的书籍嘛

怡红公子 Chrome107.0 Mac OS 10.15.7
2022-11-16 07:25:57 回复

@TheWanderingAllison: 可以的,改下这个 status 字段的值就可以了,https://github.com/lizheming/drone-doumark/blob/master/src/douban.js#L12。不过我没这个诉求,你有需要的话可以 PR 一个呀~

DeMonChat Chrome110.0 Windows 10
2023-03-03 05:41:21 回复

@怡红公子: 改了之后如何做成镜像呢

1900 Chrome106.0 Windows 10
2022-10-15 14:41:25 回复

前来感谢一下,已经利用您的工具实现了Ghost博客上的豆瓣清单

非科学の河童 Firefox99.0 Windows 10
2022-04-18 09:49:22 回复

CRON居然可以用在Github Actions上,

怡红公子 Chrome100.0 Mac OS 10.15.7
2022-04-18 10:19:06 回复

@非科学の河童: 可以的,你甚至可以在 GitHub Action 上起一个 HTTP 服务跑起来都可以(逃……

THYUU Edge100.0 Windows 10
2022-04-14 06:42:32 回复

竟然只能用 github 账号评论 😂

怡红公子 Chrome100.0 Mac OS 10.15.7
2022-04-14 07:25:59 回复

@THYUU: 嗯,因为之前被匿名评论攻击过,所以限制了下只允许 GitHub 登录了

冰剑 Chrome86.0 Windows 10
2022-04-10 12:35:05 回复

学习了。。
参考你这个,把饭否的也抓过来了。。

怡红公子 Chrome100.0 Mac OS 10.15.7
2022-04-10 13:01:09 回复

@冰剑: 冰剑大佬666,Hexo 我还没搞过呢,等你写文章分享呀嘻嘻

SHUAXIN Chrome99.0 Mac OS 10.15.7
2022-03-25 01:38:34 回复

我更好奇 老哥这个cover图是自己设计的吗 真好看

怡红公子 Chrome99.0 Mac OS 10.15.7
2022-03-25 01:40:32 回复

@SHUAXIN: 是呀,Google 搜图找的灵感,然后缝合怪一下~ 每次写文章做封面图都令人头秃 😦

林木木 Edge99.0 Mac OS 10.15.7
2022-03-22 14:34:09 回复

效果太赞啦!已完美折腾~

怡红公子 Chrome99.0 Mac OS 10.15.7
2022-03-23 14:37:51 回复

@林木木: 谢谢木木老师赞美~

Charles Chin Chrome99.0 Windows 10
2022-03-22 14:26:44 回复

这个很顶。
不过可以做一个分页截断,电影太多直接卡死。

怡红公子 Chrome99.0 Mac OS 10.15.7
2022-03-23 13:24:10 回复

@Charles Chin: 我这还行吧,做了懒加载其实还好?太长的话可以考虑渲染成 JSON 到页面之后前端做伪分页。