Hugo系列(九):添加NeoDB书影音页面
通过 Neodb API
获取书影音数据,并展示在 Hugo
页面。
前言
之前通过 doumark-action 项目获取了豆瓣观影数据,并存入博客仓库,然后展示在页面,由于豆瓣关闭了 API
,因此获取到的封面其实是项目作者反代后的地址,然而最近图片全部挂掉,后面尝试将条目封面换成 NeoDB
的封面(详情见 Hugo系列(五):添加豆瓣观影页面),但是替换会使得部署时间大幅提高,所以直接一步到位,通过 doumark-action 同步到 NeoDB
,然后使用 GitHub Action
下载数据,最后通过模板展示。
Note文章参考了:NeoDB API 创建观影页面
豆瓣同步数据到 NeoDB
豆瓣同步到 NeoDB
的方法很简单,参考 doumark-action 文档。
NeoDB 数据下载
NeoDB
的数据下载需要申请 API
,详情可以参考下面的这篇文章和官方文档:
申请到 API
后在博客仓库创建一个工作流,路径为 ~/.github/workflows/neodb.yml
1name: Sync NeoDB Data
2
3on:
4 schedule:
5 - cron: "0 17 * * *"
6 workflow_dispatch:
7
8env:
9 NEODB_DATA_PATH: assets/data/neodb
10
11jobs:
12 sync_neodb:
13 name: Sync NeoDB Data
14 runs-on: ubuntu-latest
15 steps:
16 - name: Checkout
17 uses: actions/checkout@v3
18
19 - name: Setup Environment
20 run: |
21 # 检查是否安装了 jq
22 if ! command -v jq &> /dev/null; then
23 echo "jq is not installed. Installing..."
24 sudo apt-get update
25 sudo apt-get install -y jq
26 else
27 echo "jq is already installed."
28 fi
29 # 创建数据目录
30 mkdir -p ${{ env.NEODB_DATA_PATH }}
31
32 - name: Get Local and Remote Count
33 run: |
34 # 获取本地 count
35 if [ -f "${{ env.NEODB_DATA_PATH }}/media.json" ]; then
36 LOCAL_COUNT=$(jq -r '.count // 0' ${{ env.NEODB_DATA_PATH }}/media.json)
37 else
38 LOCAL_COUNT=0
39 fi
40 echo "LOCAL_COUNT=$LOCAL_COUNT" >> $GITHUB_ENV
41
42 # 获取远程 count
43 RESPONSE=$(curl -s -X 'GET' \
44 'https://neodb.social/api/me/shelf/complete?page=1' \
45 -H 'accept: application/json' \
46 -H "Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}")
47
48 REMOTE_COUNT=$(echo "$RESPONSE" | jq -r '.count')
49 REMOTE_PAGES=$(echo "$RESPONSE" | jq -r '.pages')
50
51 echo "REMOTE_COUNT=$REMOTE_COUNT" >> $GITHUB_ENV
52 echo "REMOTE_PAGES=$REMOTE_PAGES" >> $GITHUB_ENV
53
54 - name: Sync NeoDB Data
55 if: env.LOCAL_COUNT != env.REMOTE_COUNT
56 run: |
57 mkdir -p ${{ env.NEODB_DATA_PATH }}/temp_data
58
59 for ((i=1; i<=${{ env.REMOTE_PAGES }}; i++)); do
60 url="https://neodb.social/api/me/shelf/complete?page=$i"
61 filename="${{ env.NEODB_DATA_PATH }}/temp_data/page$i.json"
62 curl -X 'GET' "$url" \
63 -H 'accept: application/json' \
64 -H "Authorization: Bearer ${{ secrets.NEODB_ACCESS_TOKEN }}" > "$filename"
65 done
66
67 # 合并所有数据到一个文件
68 jq -c -s '{
69 data: map(.data[]) | unique | sort_by(.created_time) | reverse,
70 pages: .[0].pages,
71 count: .[0].count
72 }' ${{ env.NEODB_DATA_PATH }}/temp_data/*.json > "${{ env.NEODB_DATA_PATH }}/media.json"
73
74 # 清理临时文件
75 rm -rf ${{ env.NEODB_DATA_PATH }}/temp_data
76
77 - name: Git Add and Commit
78 if: env.LOCAL_COUNT != env.REMOTE_COUNT
79 uses: EndBug/add-and-commit@v9
80 with:
81 message: 'chore(data): update neodb data'
82 add: '${{ env.NEODB_DATA_PATH }}'
然后在仓库 settings
- Secrets and variables
- Actions
下 Repository secrets
点击 New repository secret
,创建一个名称为 NEODB_ACCESS_TOKEN
的密钥,值为获取到的 NeoDB API
。
这个工作流会检查本地 ~/assets/data/neodb/media.json
中条目的数量和 NeoDB
获取到标记为完成的数量是否一致,如果不一致,则在 assets/data/neodb
下载数据并保存为 media.json
,如果一致则跳过。
NoteHugo 会在
0.136
版本舍弃get.CSV
函数,而resource.Get
获取全局数据默认路径为assets
文件夹,因此上述代码中NEODB_DATA_PATH
是在assets
中。
添加书影音页面
新建 ~/layouts\_default\media.html
并写入代码:
1{{- define "title" -}}
2 {{- title .Title -}}
3 {{- if .Site.Params.withSiteTitle }} {{ .Site.Params.titleDelimiter }} {{ .Site.Title }}{{- end -}}
4{{- end -}}
5
6{{- define "content" -}}
7 {{- $title := title .Title -}}
8 {{- $params := partial "function/params.html" -}}
9 {{- $toc := .Scratch.Get "toc" -}}
10 {{- $tocEmpty := eq .TableOfContents `<nav id="TableOfContents"></nav>` -}}
11 {{/* 样式引入 */}}
12 <link rel="stylesheet" href="/movie.css">
13 <div class="media-page">
14 <header>
15 <h1>{{ .Title }}</h1>
16 </header>
17
18 <div class="media-filters">
19 <span class="category-item active" data-category="all">全部</span>
20 <span class="category-item" data-category="movie">电影</span>
21 <span class="category-item" data-category="tv">电视剧</span>
22 <span class="category-item" data-category="book">书籍</span>
23 <span class="category-item" data-category="music">音乐</span>
24 <span class="category-item" data-category="game">游戏</span>
25 <span class="category-item" data-category="podcast">播客</span>
26 </div>
27
28 <div class="media-grid">
29 {{ $mediaResource := resources.Get "data/neodb/media.json" }}
30 {{ $media := $mediaResource | transform.Unmarshal }}
31 {{ range $media.data }}
32 {{ $title := .item.title }}
33 {{ $localizedTitles := .item.localized_title }}
34 {{ $orig_title := (index ($localizedTitles) 1).text }}
35 {{ $category := .item.category }}
36 {{ $cover := .item.cover_image_url }}
37 {{ $rating := float .item.rating }}
38 {{ $rating_percent := mul $rating 10 }}
39 {{ $rating_count := .item.rating_count }}
40 {{ $id := .item.id }}
41 {{ $brief := .item.brief }}
42 {{ $comment_text := .item.comment_text }}
43 {{ $type := .item.type }}
44
45 <div class="media-card m-cate-{{ $category }}">
46 <div class="media-cover">
47 <img class="lazyload" data-src="{{ $cover }}" alt="{{ $title }}">
48 </div>
49 <div class="media-overlay">
50 <div class="media-overlay-content">
51 <div class="media-localized-title">
52 <h2 class="media-title">
53 <a href="{{ $id }}">{{ $title }}</a>
54 </h2>
55 <span class="media-orig-title">{{ $orig_title }}</span>
56 </div>
57 <div class="media-rating">
58 <div class="stars">
59 <div class="star-bg"></div>
60 <div class="star-fill" style="width: {{ $rating_percent }}%"></div>
61 </div>
62 </div>
63 <div class="media-rating-info">
64 <span class="rating-num">{{ $rating }} 分</span>
65 <span class="rating-count">{{ $rating_count }} 人点评</span>
66 </div>
67 </div>
68 <div class="media-brief">
69 {{ if $comment_text }}
70 <p>短评:{{ $comment_text }}</p>
71 {{ else }}
72 <p>简介:{{ $brief }}</p>
73 {{ end }}
74 </div>
75 <div class="media-type">
76 {{ $type }}
77 </div>
78 </div>
79 </div>
80 {{ end }}
81 </div>
82 </div>
83
84
85 {{/* 图片懒加载 */}}
86 <script src='{{ "/js/lazyload.iife.min.js" | relURL }}'></script>
87 <script>
88 document.addEventListener("DOMContentLoaded", function() {
89 new LazyLoad({
90 elements_selector: ".lazyload"
91 });
92 });
93 </script>
94
95 {{/* 类别过滤 */}}
96 <script>
97 const buttons = document.querySelectorAll('.category-item');
98 const cards = document.querySelectorAll('.media-card');
99
100 buttons.forEach(button => {
101 button.addEventListener('click', (event) => {
102 const category = event.target.dataset.category;
103 buttons.forEach(btn => {
104 btn.classList.remove('active');
105 });
106 event.target.classList.add('active');
107
108 if (category === 'all') {
109 cards.forEach(card => {
110 card.classList.remove('hidden');
111 });
112 } else {
113 cards.forEach(card => {
114 if (card.classList.contains(`m-cate-${category}`)) {
115 card.classList.remove('hidden');
116 } else {
117 card.classList.add('hidden');
118 }
119 });
120 }
121 });
122 });
123 </script>
124{{ end }}
Tip样式引入前的代码和最后的
{{ end }}
是本主题的模板,其他主题需要更换
在 ~/static
文件夹下新建 media.css
文件,写入样式:
1/* media.css */
2.media-page {
3 max-width: 1200px;
4 margin: 0 auto;
5 padding: 20px;
6}
7
8.media-filters {
9 margin-bottom: 20px;
10 display: flex;
11 flex-wrap: wrap;
12}
13
14.category-item {
15 cursor: pointer;
16 margin-right: 10px;
17 padding: 5px 10px;
18 border-radius: 5px;
19 background-color: #f3f4f6;
20 color: #334155;
21 font-size: 0.875rem;
22 line-height: 1.25rem;
23 margin-bottom: 0.5rem;
24}
25.category-item:hover {
26 background-color: #e2e8f0;
27}
28
29.category-item.active {
30 background-color: #4b5563;
31 color: #f3f4f6;
32}
33
34.media-grid {
35 display: grid;
36 grid-template-columns: repeat(5, 1fr);
37 gap: 10px;
38}
39
40.media-card {
41 position: relative;
42 border-radius: 5px;
43 overflow: hidden;
44 transition: transform 0.5s ease, box-shadow 0.5s ease;
45 background-color: rgba(255, 255, 255, 0.2); /* 半透明背景 */
46 box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
47}
48
49.media-card:hover {
50 transform: scale(1.05) translateY(-8px); /* 悬浮时放大并上移 */
51 box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); /* 增强悬浮时的阴影效果 */
52}
53
54/* 媒体封面 */
55.media-cover {
56 position: relative;
57 overflow: hidden;
58 aspect-ratio: 2 / 3;
59 transition: filter 0.5s ease; /* 过渡效果应用于模糊和亮度 */
60}
61
62.media-cover img {
63 width: 100%;
64 height: 100%;
65 object-fit: cover;
66 transition: filter 0.5s ease; /* 添加图片模糊和亮度过渡 */
67}
68
69.media-card:hover .media-cover img {
70 filter: blur(5px) brightness(75%); /* 仅对封面图片应用模糊和亮度降低 */
71}
72
73
74.media-overlay {
75 position: absolute;
76 top: 0;
77 left: 0;
78 right: 0;
79 bottom: 0;
80 background-color: rgba(0, 0, 0, 0.7);
81 color: #fff;
82 opacity: 0;
83 transition: opacity 0.3s ease;
84 display: flex;
85 flex-direction: column;
86 justify-content: center;
87 align-items: flex-start;
88 text-align: left;
89 padding: 10px;
90}
91
92.media-card:hover .media-overlay {
93 opacity: 1;
94}
95
96.media-overlay-content {
97 width: 100%;
98}
99
100.media-localized-title h2 {
101 font-size: 1.25rem;
102 font-weight: 500;
103 margin: 0;
104}
105.media-localized-title a {
106 color: #f3f4f6;
107}
108.media-localized-title a:hover {
109 text-decoration: underline;
110 color: #ff9800;
111}
112
113.media-orig-title {
114 font-size: 0.875rem;
115 color: #f3f4f6;
116}
117
118.stars {
119 position: relative;
120 display: inline-block;
121 width: 80px; /* 星星的总宽度 */
122 height: 16px; /* 星星的高度 */
123}
124
125.star-bg, .star-fill {
126 position: absolute;
127 top: 0;
128 left: 0;
129 height: 100%;
130 background-size: 16px 16px; /* 星星的大小 */
131 background-repeat: repeat-x;
132}
133
134.star-bg {
135 width: 100%;
136 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==);
137}
138
139.star-fill {
140 width: 100%;
141 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);
142}
143
144.media-rating-info span {
145 font-size: 0.875rem;
146 margin-top: 2px;
147 background-color: #334155;
148 border-radius: 3px;
149 padding: 3px;
150}
151
152.media-brief p {
153 font-size: 14px;
154 margin: 10px 0;
155 overflow: hidden;
156 text-overflow: ellipsis;
157 display: -webkit-box;
158 -webkit-line-clamp: 3;
159 -webkit-box-orient: vertical;
160}
161
162.media-type {
163 position: absolute;
164 top: 0;
165 right: 0;
166 background-color: #ff9800;
167 color: #fff;
168 padding: 5px 10px;
169 border-radius: 0 0 0 5px;
170 opacity: 0;
171 transition: opacity 0.3s ease;
172}
173
174.media-card:hover .media-type {
175 opacity: 1;
176}
177
178.hidden {
179 display: none;
180}
181
182@media (max-width: 600px) {
183 .media-grid {
184 grid-template-columns: repeat(2, 1fr);
185 gap: 8px; /* 减小卡片间距 */
186 }
187
188 .media-card:hover {
189 transform: scale(1.03) translateY(-4px); /* 减小悬浮效果 */
190 }
191
192 .media-overlay {
193 padding: 6px; /* 减小内边距 */
194 }
195
196 .media-localized-title h2 {
197 font-size: 1rem; /* 减小标题字体大小 */
198 }
199
200 .media-orig-title {
201 font-size: 0.75rem; /* 减小原标题字体大小 */
202 }
203
204 .star {
205 font-size: 1rem; /* 减小星星大小 */
206 }
207
208 .media-rating-info span {
209 font-size: 0.75rem; /* 减小评分信息字体大小 */
210 padding: 2px; /* 减小评分信息内边距 */
211 }
212
213 .media-brief p {
214 font-size: 12px; /* 减小简介字体大小 */
215 margin: 6px 0; /* 减小上下间距 */
216 -webkit-line-clamp: 2; /* 减少显示行数 */
217 }
218
219 .media-type {
220 padding: 3px 6px; /* 减小类型标签内边距 */
221 font-size: 0.75rem; /* 减小类型标签字体大小 */
222 }
223
224 .category-item {
225 padding: 3px 6px; /* 减小分类按钮内边距 */
226 font-size: 0.75rem; /* 减小分类按钮字体大小 */
227 margin-right: 6px; /* 减小分类按钮右侧间距 */
228 margin-bottom: 0.3rem; /* 减小分类按钮下方间距 */
229 }
230}
最后在 ~/static/js/
文件夹下放入 lazyload.iife.min.js
代码文件,可以点击此链接下载保存,上述代码中已经包括了样式文件和懒加载文件的引入。
完成后新建 ~/content/media.md
文件,写入信息:
1---
2title: 书影音记录
3layout: "media"
4---
最后在 Hugo
站点配置中添加菜单即可。