forked from
quillmatiq.com/augment
Fork of Chiri for Astro for my blog
1<script is:inline>
2 function loadNeoDBCards() {
3 const allContainers = document.querySelectorAll('.neodb-card-container')
4
5 allContainers.forEach((container) => {
6 if (!container.hasAttribute('data-url')) {
7 container.remove()
8 }
9 })
10
11 const containers = document.querySelectorAll('.neodb-card-container[data-url]')
12
13 if (containers.length === 0) {
14 return
15 }
16
17 const renderError = (elem) => {
18 elem.innerHTML = `<div class="neodb-card error"><p class="neodb-error">🔴 Error</p></div>`
19 }
20
21 const renderCard = (elem, data, targetUrl) => {
22 const itemRating = data?.rating && data.rating > 0 ? parseFloat(data.rating) : null
23 const coverUrl = data.cover_image_url || data.cover_url
24 const category = data.category || ''
25
26 const getPreferredTitle = () => {
27 // Special handling for music/album types only
28 if (category === 'music' || category === 'album') {
29 if (Array.isArray(data.localized_title)) {
30 const nonZh = data.localized_title.find((t) => t.lang !== 'zh-cn' && t.text)
31 if (nonZh) return nonZh.text
32 const zh = data.localized_title.find((t) => t.lang === 'zh-cn' && t.text)
33 if (zh) return zh.text
34 }
35 }
36 return data.title || data.name || ''
37 }
38
39 const title = getPreferredTitle()
40
41 const isSquare = category === 'music' || category === 'podcast'
42 const coverClass = isSquare ? 'music' : 'other'
43 const cardHeightClass = isSquare ? 'compact' : 'standard'
44
45 // Convert rating to star percentage with half-star precision: 10 points = 100% (5 stars)
46 const starPercentage = itemRating !== null ? Math.round((itemRating / 10) * 10) * 10 : null
47
48 // Build field info
49 const fieldInfo = []
50
51 if (category === 'movie' || category === 'tv' || category === 'tv/season') {
52 // Movie/TV: Director, Cast, Genre, Release Year
53 if (data.director && data.director.length > 0) {
54 const directors = Array.isArray(data.director) ? data.director.join(', ') : data.director
55 fieldInfo.push(`导演: ${directors}`)
56 }
57 if (data.actor && data.actor.length > 0) {
58 const actors = Array.isArray(data.actor) ? data.actor.join(', ') : data.actor
59 fieldInfo.push(`演员: ${actors}`)
60 }
61 if (data.genre && data.genre.length > 0) {
62 const genres = Array.isArray(data.genre) ? data.genre.join(', ') : data.genre
63 fieldInfo.push(`类型: ${genres}`)
64 }
65 // Add release year - "Release Date" for movies, "Premiere" for TV
66 if (data.year) {
67 const timeLabel = category === 'movie' ? '上映时间' : '首播'
68 fieldInfo.push(`${timeLabel}: ${data.year}`)
69 }
70 } else if (category === 'music' || category === 'album') {
71 // Music: Artist, Genre, Release Date
72 if (data.artist && data.artist.length > 0) {
73 // Only show the first artist for music/album
74 const artist = Array.isArray(data.artist) ? data.artist[0] : data.artist
75 fieldInfo.push(`艺术家: ${artist}`)
76 }
77 if (data.genre && data.genre.length > 0) {
78 const genres = Array.isArray(data.genre) ? data.genre.join(', ') : data.genre
79 fieldInfo.push(`流派: ${genres}`)
80 }
81 if (data.release_date) {
82 fieldInfo.push(`发行时间: ${data.release_date}`)
83 }
84 } else if (category === 'book') {
85 // Book: Author, Publisher
86 if (data.author && data.author.length > 0) {
87 const authors = Array.isArray(data.author) ? data.author.join(', ') : data.author
88 fieldInfo.push(`作者: ${authors}`)
89 }
90 // Check multiple possible publisher fields
91 const publisher = data.publisher || data.pub_house || data.company
92 if (publisher && (Array.isArray(publisher) ? publisher.length > 0 : publisher)) {
93 const publishers = Array.isArray(publisher) ? publisher.join(', ') : publisher
94 fieldInfo.push(`出版社: ${publishers}`)
95 }
96 } else if (category === 'game') {
97 // Game: Developer, Genre, Release Date
98 if (data.developer && data.developer.length > 0) {
99 const developers = Array.isArray(data.developer) ? data.developer.join(', ') : data.developer
100 fieldInfo.push(`开发者: ${developers}`)
101 }
102 if (data.genre && data.genre.length > 0) {
103 const genres = Array.isArray(data.genre) ? data.genre.join(', ') : data.genre
104 fieldInfo.push(`类型: ${genres}`)
105 }
106 if (data.release_date) {
107 fieldInfo.push(`发行时间: ${data.release_date}`)
108 }
109 } else if (category === 'podcast') {
110 // Podcast: Host, Genre
111 const host = data.host || data.artist || data.creator
112 if (host && (Array.isArray(host) ? host.length > 0 : host)) {
113 const hosts = Array.isArray(host) ? host.join(', ') : host
114 fieldInfo.push(`主播: ${hosts}`)
115 }
116 if (data.genre && data.genre.length > 0) {
117 const genres = Array.isArray(data.genre) ? data.genre.join(', ') : data.genre
118 fieldInfo.push(`类型: ${genres}`)
119 }
120 }
121
122 elem.innerHTML = `
123 <a class="neodb-card ${cardHeightClass}" href="${targetUrl}" target="_blank" rel="noopener noreferrer">
124 ${coverUrl ? `<img class="neodb-cover ${coverClass}" src="${coverUrl}" alt="${title || ''}" loading="lazy" />` : `<div class="neodb-cover ${coverClass}" style="background: #f3f4f6;"></div>`}
125 <div class="neodb-info">
126 <div class="neodb-title">${title || ''}</div>
127 ${
128 starPercentage !== null
129 ? `<div class="rating">
130 <span class="allstarbg">
131 <span class="allstarfg" style="width:${starPercentage}%"></span>
132 </span>
133 <span class="rating_nums">${itemRating.toFixed(1)}</span>
134 </div>`
135 : `<div class="rating">
136 <span class="allstargray"></span>
137 </div>`
138 }
139 ${fieldInfo
140 .map((info) => {
141 if (info.startsWith('<div class="neodb-desc">')) {
142 return info
143 }
144 return `<div class="neodb-field">${info}</div>`
145 })
146 .join('')}
147 </div>
148 </a>
149 `
150 }
151
152 // Build fetch promises for parallel execution
153 const fetchPromises = Array.from(containers).map(async (container) => {
154 const url = container.getAttribute('data-url')
155
156 if (!url) return
157
158 let fetchUrl = ''
159 const neodbUrlPattern = /neodb\.social\/(movie|book|music|album|game|tv\/season|tv|podcast)\/([\w-]+)/
160 const isNeoDbUrl = neodbUrlPattern.test(url)
161
162 if (isNeoDbUrl) {
163 const match = url.match(neodbUrlPattern)
164 let category = match ? match[1] : ''
165 const uuid = match ? match[2] : ''
166
167 if (category === 'tv/season') {
168 category = 'tv/season'
169 }
170
171 if (uuid && category) {
172 fetchUrl = `https://neodb.social/api/${category}/${uuid}`
173 }
174 } else {
175 fetchUrl = `https://neodb.social/api/catalog/fetch?url=${encodeURIComponent(url)}`
176 }
177
178 if (fetchUrl) {
179 try {
180 const res = await fetch(fetchUrl, {
181 mode: 'cors',
182 headers: {
183 Accept: 'application/json'
184 }
185 })
186 if (!res.ok) throw new Error(`Response ${res.status}`)
187 const json = await res.json()
188 const data = json.data ? json.data : json
189 renderCard(container, data, url)
190 } catch {
191 renderError(container)
192 }
193 } else {
194 renderError(container)
195 }
196 })
197
198 // Execute all fetches in parallel
199 Promise.allSettled(fetchPromises)
200 }
201
202 loadNeoDBCards()
203 document.addEventListener('astro:page-load', loadNeoDBCards)
204</script>
205
206<style is:inline>
207 .prose .neodb-card {
208 width: 100%;
209 height: 100%;
210 background: var(--astro-code-background);
211 color: var(--text-primary);
212 border-radius: 8px;
213 display: flex;
214 position: relative;
215 text-decoration: none !important;
216 transition: background 0.2s ease-out;
217 margin: 1.25rem 0 1.75rem 0;
218 overflow: hidden;
219 }
220
221 .prose .neodb-card.compact {
222 min-height: 6rem;
223 }
224
225 .prose .neodb-card.standard {
226 min-height: 9rem;
227 }
228
229 .prose .neodb-card:hover {
230 background: color-mix(in srgb, var(--selection) 75%, transparent);
231 }
232
233 .prose .neodb-card.error:hover {
234 background: var(--astro-code-background) !important;
235 cursor: default;
236 }
237
238 .prose .neodb-cover {
239 width: 96px !important;
240 border-radius: 6px;
241 margin: 1rem;
242 }
243
244 .prose .neodb-cover.music {
245 height: 96px !important; /* 1:1 for music */
246 }
247
248 .prose .neodb-cover.other {
249 height: 144px !important; /* 9:16 for other types */
250 }
251
252 .prose .neodb-info {
253 height: 100%;
254 display: flex;
255 flex-direction: column;
256 gap: 0.25rem;
257 flex: 1;
258 min-width: 0;
259 overflow: hidden;
260 margin: 1rem 1rem 1rem 0;
261 }
262
263 .prose .neodb-field {
264 font-size: var(--font-size-s);
265 color: var(--text-primary);
266 line-height: 1.4;
267 white-space: nowrap;
268 overflow: hidden;
269 text-overflow: ellipsis;
270 }
271
272 .prose .neodb-artist {
273 font-size: var(--font-size-s);
274 color: var(--text-primary);
275 }
276
277 .prose .no-rating {
278 font-size: var(--font-size-s);
279 color: var(--text-primary);
280 line-height: 1;
281 margin: 0 0 0.25rem 0;
282 }
283
284 .prose .neodb-title {
285 color: var(--text-primary);
286 font-weight: var(--font-weight-bold);
287 line-height: 1.35;
288 margin-bottom: 0.125rem;
289 }
290
291 .prose .neodb-title a {
292 text-decoration: none !important;
293 }
294
295 .prose .neodb-desc {
296 font-size: var(--font-size-s);
297 color: var(--text-primary);
298 margin-top: 0.25rem;
299 display: -webkit-box;
300 -webkit-line-clamp: 2;
301 -webkit-box-orient: vertical;
302 text-overflow: ellipsis;
303 overflow: hidden;
304 word-wrap: break-word;
305 max-width: 100%;
306 }
307
308 .prose .rating {
309 margin: 0 0 0.25rem 0;
310 font-size: var(--font-size-s);
311 line-height: 1;
312 display: flex;
313 align-items: center;
314 }
315
316 .prose .rating .allstarbg {
317 position: relative;
318 color: #f99b01;
319 height: 1rem;
320 width: 5rem;
321 background-size: auto 100%;
322 margin-right: 0.25rem;
323 background-repeat: repeat;
324 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6TTY2NC44IDU2MS42bDM2LjEgMjEwLjNMNTEyIDY3Mi43IDMyMy4xIDc3MmwzNi4xLTIxMC4zLTE1Mi44LTE0OUw0MTcuNiAzODIgNTEyIDE5MC43IDYwNi40IDM4MmwyMTEuMiAzMC43LTE1Mi44IDE0OC45eiIgZmlsbD0iI2Y5OWIwMSIvPjwvc3ZnPg==);
325 }
326
327 .prose .rating .allstarfg {
328 position: absolute;
329 left: 0;
330 color: #f99b01;
331 height: 1rem;
332 overflow: hidden;
333 background-size: auto 100%;
334 background-repeat: repeat;
335 background-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZjk5YjAxIi8+PC9zdmc+);
336 }
337
338 .prose .rating_nums {
339 font-size: var(--font-size-s);
340 color: var(--text-primary);
341 }
342
343 .prose .rating .allstargray {
344 position: relative;
345 height: 1rem;
346 width: 5rem;
347 margin-right: 8px;
348 background-color: var(--text-primary);
349 opacity: 0.15;
350 -webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+);
351 mask-image: url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik05MDguMSAzNTMuMWwtMjUzLjktMzYuOUw1NDAuNyA4Ni4xYy0zLjEtNi4zLTguMi0xMS40LTE0LjUtMTQuNS0xNS44LTcuOC0zNS0xLjMtNDIuOSAxNC41TDM2OS44IDMxNi4ybC0yNTMuOSAzNi45Yy03IDEtMTMuNCA0LjMtMTguMyA5LjMtMTIuMyAxMi43LTEyLjEgMzIuOS42IDQ1LjNsMTgzLjcgMTc5LjEtNDMuNCAyNTIuOWMtMS4yIDYuOS0uMSAxNC4xIDMuMiAyMC4zIDguMiAxNS42IDI3LjYgMjEuNyA0My4yIDEzLjRMNTEyIDc1NGwyMjcuMSAxMTkuNGM2LjIgMy4zIDEzLjQgNC40IDIwLjMgMy4yIDE3LjQtMyAyOS4xLTE5LjUgMjYuMS0zNi45bC00My40LTI1Mi45IDE4My43LTE3OS4xYzUtNC45IDguMy0xMS4zIDkuMy0xOC4zIDIuNy0xNy41LTkuNS0zMy43LTI3LTM2LjN6IiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+);
352 -webkit-mask-size: auto 100%;
353 mask-size: auto 100%;
354 -webkit-mask-repeat: repeat;
355 mask-repeat: repeat;
356 }
357
358 /* Skeleton/Loading card styles */
359 .prose .neodb-loading.music {
360 min-height: 8rem !important;
361 background: var(--astro-code-background);
362 display: flex;
363 align-items: center;
364 justify-content: center;
365 }
366
367 .prose .neodb-loading.other {
368 min-height: 11rem !important;
369 background: var(--astro-code-background);
370 display: flex;
371 align-items: center;
372 justify-content: center;
373 }
374
375 .prose .neodb-error {
376 font-size: var(--font-size-m);
377 margin: 1.5rem;
378 }
379</style>