早期实现

豆瓣观影清单、书架的功能之前也做过,当时用过好几个方案

  1. 当时找的一个教程,不过不记得出处了,不过我写了一篇日志记录这个事情,里面记录了实现过程,原理其实很现在这个类似
  2. 另外一个是当时一个叫mufeng还是布克牧为的大佬搞了一个douban的数据站,可以缓存你豆瓣的观影数据,不过好像也停止服务了

这两个方案都或多或少的出现问题了,我后来懒也就一直没有修过,所以之前导航上一直没有书架和豆瓣的链接。

其他方案

但是在空着的这段时间我有时候也找过解决方案,有一次在木木木木木 大佬的博客里发现了一个hugo的实现方案,功能非常齐全,可以实现清单的分类筛选、时间筛选、评分筛选、排序等功能。

当时一下就心动了。

大佬博客也有一篇日志提到了这个功能是谁写的,并且附上了链接。

来自于 @怡红公子 的自制轮子:doumark-action ,豆瓣书影音同步 GitHub Action。

我当时尝试着弄了一下,虽然成功通过doumark-action 缓存了我的豆瓣数据,但是后续的页面渲染把我卡住了,hugo的模板渲染用到了一些hugo特有的函数,我当时也看不太懂,所以就暂时搁置了。

后来有些时候想起了会忽然研究一下怎么处理,但是一直没落实代码实现上面。


不过今天终于下定决心要把这个功能给实现出来。

仔细研究了hugo的渲染代码,我发现其实前端的渲染完全可以用我目前渲染Memos的方式实现,只需要把数据处理成和hugo用到的格式就行了。

idtitlesubtitleposterpubdateurlratinggenresstarcommenttagsstar_time
3469612151杰伊·比姆2021 / 印度 / 剧情 犯罪 / 塔·塞·纳纳维尔 / 苏利耶·西瓦库马 Lijo Mol Josehttps://img9.doubanio.com/view/photo/m_ratio_poster/public/p2734251414.jpg2021-11-02(印度)https://movie.douban.com/subject/35652715/8.7剧情,犯罪5这,如此显然的诬陷,竟然盛行!2022-08-23 10:12:03

后来我又研究了一下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
                         })
    })
    
})

实现开始

缓存豆瓣数据

这里我就不写了,引用@怡红公子 大佬的教程,唯一的区别是我更改了 formatjson

使用其实很简单,在你的博客仓库中新建 .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 同步豆瓣账号 lizhemingmovie 类型数据到 ./douban 文件夹下,并保存为 json 格式文件;
  • 最后一步则是当 ./douban 文件夹下有更新则调用插件提交修改。

转化数据

完整代码在下面

  1. 在你的主题里添加一个新页面 page-movies.hbs 并添加页面常规代码
  2. 这里我添加了一个主题自定义判断,如果设置了json数据文件的地址才做数据转换
  3. 定义一个临时变量 temp 用于存放最终转换后的数据
  4. 通过JQuery getJSON 方法请求json文件。
  5. 通过 each 方法遍历数据,并从JSON数据里拿去到想要的数据,按hugo那边的格式组成 JSON 对象,并存在变量 temp.movies 里。
  6. 因为取到的分类数据是 剧情,动画 这种带 , 号分隔符的,而之后筛选需要用到单独标签名称,所以需要做下处理,我这里重新遍历了 movies 对象,如果有多个标签的则用 split 做分割,并且通过 indexOf 避免重复,如果有了就不再重复添加。
  7. 另外这里也顺带处理了一下 year 字段,原因和分类一样,之后筛选要用到,所以要去重复,并且做了截取处理。
  8. 通过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 script部分代码

添加样式和模板代码

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