Ghost 博客实现豆瓣观影清单

Ghost 博客实现豆瓣观影清单

October 15, 2022
分享 , 设计 ,

早期实现

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

  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

加入评论