Ghost博客实现豆瓣观影清单
早期实现
豆瓣观影清单、书架的功能之前也做过,当时用过好几个方案
- 当时找的一个教程,不过不记得出处了,不过我写了一篇日志记录这个事情,里面记录了实现过程,原理其实很现在这个类似
- 另外一个是当时一个叫mufeng还是布克牧为的大佬搞了一个douban的数据站,可以缓存你豆瓣的观影数据,不过好像也停止服务了
这两个方案都或多或少的出现问题了,我后来懒也就一直没有修过,所以之前导航上一直没有书架和豆瓣的链接。
其他方案
但是在空着的这段时间我有时候也找过解决方案,有一次在木木木木木 大佬的博客里发现了一个hugo的实现方案,功能非常齐全,可以实现清单的分类筛选、时间筛选、评分筛选、排序等功能。
当时一下就心动了。

大佬博客也有一篇日志提到了这个功能是谁写的,并且附上了链接。
来自于 @怡红公子 的自制轮子:doumark-action ,豆瓣书影音同步 GitHub Action。
我当时尝试着弄了一下,虽然成功通过doumark-action 缓存了我的豆瓣数据,但是后续的页面渲染把我卡住了,hugo的模板渲染用到了一些hugo特有的函数,我当时也看不太懂,所以就暂时搁置了。
后来有些时候想起了会忽然研究一下怎么处理,但是一直没落实代码实现上面。
不过今天终于下定决心要把这个功能给实现出来。
仔细研究了hugo的渲染代码,我发现其实前端的渲染完全可以用我目前渲染Memos的方式实现,只需要把数据处理成和hugo用到的格式就行了。
后来我又研究了一下doumark-action,发现可以把数据缓存成JSON,而且因为GitHub又raw格式的链接,可以提供外链。所以我就灵机一动:我可不可前端请求JSON数据,然后把JSON转换成hugo渲染用到的那种格式?
经过测试后发现确实可以实现数据转换,所以就有了今天这个方案。
var temp = { movies : [] }
$.getJSON('https://jsd.cdn.zzko.cn/gh/rebron1900/doumark-action@master/data/douban/movie.json',function(r){
$(r).each(function(){
temp.movies.push({id : this.id,
title : this.subject.title,
subtitle : this.subject.card_subtitle,
poster: this.subject.pic.large,
pubdate: this.subject.pubdate[0],
url: this.subject.url,
rating: this.subject.rating.value,
genres: this.subject.genres.join(','),
star: this.subject.rating.star,
comment: this.comment,
tags: this.tags.join(','),
star_time: this.create_time
})
})
})
实现开始
缓存豆瓣数据
这里我就不写了,引用@怡红公子 大佬的教程,唯一的区别是我更改了 format
为 json
。
使用其实很简单,在你的博客仓库中新建 .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: json
dir: ./douban
- name: Commit
uses: EndBug/add-and-commit@v8
with:
message: 'chore: update douban data'
add: './douban'
该 workflow 总共分为三步,
- 第一步初始化 Git 仓库;
- 第二步调用 doumark-action 同步豆瓣账号
lizheming
的movie
类型数据到./douban
文件夹下,并保存为json
格式文件; - 最后一步则是当
./douban
文件夹下有更新则调用插件提交修改。
转化数据
完整代码在下面
- 在你的主题里添加一个新页面
page-movies.hbs
并添加页面常规代码 - 这里我添加了一个主题自定义判断,如果设置了json数据文件的地址才做数据转换
- 定义一个临时变量
temp
用于存放最终转换后的数据 - 通过JQuery
getJSON
方法请求json文件。 - 通过
each
方法遍历数据,并从JSON数据里拿去到想要的数据,按hugo那边的格式组成JSON
对象,并存在变量temp.movies
里。 - 因为取到的分类数据是
剧情,动画
这种带,
号分隔符的,而之后筛选需要用到单独标签名称,所以需要做下处理,我这里重新遍历了movies
对象,如果有多个标签的则用split
做分割,并且通过indexOf
避免重复,如果有了就不再重复添加。 - 另外这里也顺带处理了一下
year
字段,原因和分类一样,之后筛选要用到,所以要去重复,并且做了截取处理。 - 通过javascript-template来渲染数据
{{#if @custom.douban_movie}}
<script>
$(document).ready(function () {
var temp = { movies : [] }
$.getJSON('{{ @custom.douban_movie }}',function(r){
$(r).each(function(){
temp.movies.push({id : this.id,
title : this.subject.title,
subtitle : this.subject.card_subtitle,
poster: this.subject.pic.large,
pubdate: this.subject.pubdate[0],
url: this.subject.url,
rating: this.subject.rating.value,
genres: this.subject.genres.join(','),
star: this.subject.rating.star_count,
comment: this.comment,
tags: this.tags.join(','),
star_time: this.create_time
})
})
$('.movies').html(tmpl('tmpl-movies', temp))
var gtemp = [];
var gyear = [];
$(temp.movies).each(function(){
t = this.genres.split(',');
if(t.length > 0){
$(t).each(function(i,d){
if(gtemp.indexOf(d) == -1){ gtemp.push(d) }
});
}else{
gtemp.push(this.genres) }
var tyear = this.subtitle.substring(0,4);
if(gyear.indexOf(tyear) == -1 ){
gyear.push(tyear)
}
});
$('.genres').html(tmpl('tmpl-genres', gtemp))
$('.fyear').html(tmpl('tmpl-fyear', gyear.sort(function(a,b){ return b-a } )))
})
});
</script>
{{/if}}
添加样式和模板代码
在 page-movies.hbs
中添加样式代码,这里的代码我都是直接复制hugo实现那边的,可以完美使用,不需要做任何改动。
<!-- 样式代码 -->
<style>
.gFnzgG,.gFnzgG *{box-sizing:border-box}
.fIuTG{display:flex;flex-wrap:wrap;margin:0 -2%;background:0 0;line-height:100%;justify-content: center;}
.dfdORB{width:150px;margin:0 2% 30px;padding:0;font-size:15px}
.dfdORB a{text-decoration:none}
.kMthTr{margin-top:12px;line-height:1.3;max-height:2.6rem;overflow:hidden}
.eysHZq{display:flex;-webkit-box-align:center;align-items:center;margin-top:5px;min-height:16px;line-height:1;-webkit-box-align: center;}
.HPRth{position:relative;min-height:87px;overflow:hidden;color:transparent;width: 150px;height: 230px;}
.HPRth:hover{box-shadow:rgb(48 55 66 / 30%) 0 1rem 2.1rem}
.jcTaHb{display:flex;-webkit-box-align:center;align-items:center}
.lhtmRw{margin-right:1px;width:12px;height:12px;color:#fccd59}
.gaztka{margin-right:1px;width:12px;height:12px;color:#eee}
.iibjPt{margin-left:5px;color:#555;font-size:14px}
.jvCTkj{margin-bottom:10px}
.kEoOHR{display:inline-block;margin-right:15px;text-decoration:none;color:#157efb}
.dvtjjf{display:inline-block;color:#555;text-decoration:none;padding:0 5px}
.dvtjjf.active{background:rgba(85,85,85,.1)}
.hide{display:none}
.sort-by{text-align:right;margin-top:-15px}
.sort-by-item{margin-left:10px;padding:0 5px;line-height:20px;text-decoration: none;color: var(--color-content-secondary);}
.sort-by-item.active{background:rgba(85,85,85,.1)}
.sort-by-item svg{margin-right:5px}
.sc-hKFxyN img{max-width:100%!important;height:100%!important;display:block!important;vertical-align:middle!important;margin: 0;padding: 0;}
.lazyload-wrapper {height: 100%;}
@media(min-width:1024px){
.lg\:col-span-6{grid-column:span 6/span 6!important}
.lg\:col-start-2{grid-column-start:2!important}
}
@media (max-width:550px){
.jcTaHb,.sc-bdnxRM{display:none}
}
</style>
<!-- 筛选功能用到的js -->
<script type="text/javascript">
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');
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);
}
});
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);
}
});
</script>
渲染筛选器和电影信息
这里我是按着hugo的代码逻辑,用tmpl做了实现,都是输出数据而已,就不细说了。其中 year
部分做了一下空值判断。
<script type="text/x-tmpl" id="tmpl-movies">
{% for (var i=0; i<o.movies.length; i++) { %}
<div
class="sc-gKAaRy dfdORB hint--top hint--medium"
data-year="{%= typeof(o.movies[i].pubdate) == "undefined" ? "":o.movies[i].pubdate.substring(0,4) %}"
data-star="{%= o.movies[i].star %}"
data-rating="{%= o.movies[i].rating %}"
data-genres="{%= o.movies[i].genres %}"
aria-label="{%= o.movies[i].comment %}"
>
<a rel="link" href="{%= o.movies[i].url %}" target="_blank">
<div class="sc-hKFxyN HPRth">
<div class="lazyload-wrapper ">
<img class="avatar" src="{%= o.movies[i].poster %}" referrer-policy="no-referrer" loading="lazy" alt="{%= o.movies[i].title %}" width="150" height="220">
</div>
</div>
<div class="sc-iCoGMd kMthTr">{%= o.movies[i].title %}</div>
<div class="sc-fujyAs eysHZq">
<span class="sc-jSFjdj jcTaHb">
{% for (var b=0; b<5; b++) { %}
<svg viewBox="0 0 24 24" width="24" height="24" class="sc-dlnjwi {% if (b < o.movies[i].star) { %} lhtmRw {% }else{ %} gaztka {% } %} ">
<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>
{% } %}
</span>
<span class="sc-pNWdM iibjPt">{%= o.movies[i].rating %}</span>
</div>
</a>
</div>
{% } %}
</script>
<script type="text/x-tmpl" id="tmpl-genres">
<a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="genres" data-method="contain" data-value="">全部</a>
{% for (var i=0; i<o.length; i++) { %}
<a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="genres" data-method="contain" data-value="{%= o[i] %}">{%= o[i] %}</a>
{% } %}
</script>
<script type="text/x-tmpl" id="tmpl-fyear">
<a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="year" data-method="equal" data-value="">全部</a>
{% for (var i=0; i<o.length; i++) { %}
<a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="year" data-method="equal" data-value="{%= o[i] %}">{%= o[i] %}</a>
{% } %}
</script>
完整代码请查看Github,或者查看 page-movies.hbs
https://github.com/rebron1900/attila/blob/master/page-movies.hbs
加入评论