muriel 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/examples/catpea-blog-profile.json +34 -5
- package/examples/muriel-blog-maker/blog.js +14 -11
- package/examples/muriel-blog-maker/lib.js +29 -20
- package/examples/muriel-blog-maker/transforms/homepage/SNIPPETS.md +8 -0
- package/examples/muriel-blog-maker/transforms/homepage/index.js +13 -30
- package/examples/muriel-blog-maker/transforms/pagerizer/index.js +21 -40
- package/examples/muriel-blog-maker/transforms/post-scanner/index.js +13 -4
- package/examples/muriel-blog-maker/transforms/process-audio/index.js +26 -3
- package/examples/muriel-blog-maker/transforms/process-cover/index.js +46 -14
- package/examples/muriel-blog-maker/transforms/skip-unchanged/index.js +1 -0
- package/examples/muriel-blog-maker/transforms/use-theme/index.js +42 -0
- package/examples/sync-database.js +0 -1
- package/index.js +25 -3
- package/package.json +1 -1
- package/catpea_www/.muriel-manifest.json +0 -366
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ No nodes. No operators. No DSL gymnastics.
|
|
|
17
17
|
- **Everything else is series**
|
|
18
18
|
- **Worker threads are optional**
|
|
19
19
|
|
|
20
|
+
NOTE: The worker pool is more suited for CPU-bound pure-JS transforms where you want to avoid blocking the main thread — e.g., heavy string processing, JSON transformations, or math-intensive filters across thousands of packets.
|
|
21
|
+
|
|
20
22
|
---
|
|
21
23
|
|
|
22
24
|
## Transform API
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
{
|
|
2
|
+
|
|
3
|
+
"debug":{
|
|
4
|
+
|
|
5
|
+
"_processOnly": ["poem-2074", "poem-2077", "poem-2094", "poem-2095", "poem-2111"],
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
"mostRecent": 48,
|
|
9
|
+
"skipCovers": true,
|
|
10
|
+
"skipAudio": true
|
|
11
|
+
},
|
|
12
|
+
|
|
2
13
|
"profile": "catpea_www",
|
|
3
14
|
"title": "Cat Pea",
|
|
4
15
|
|
|
5
16
|
"src": "examples/catpea-blog-sample-data/database/main-posts",
|
|
6
17
|
"dest": "examples/catpea-blog-sample-data/dist/{profile}",
|
|
7
|
-
|
|
18
|
+
|
|
19
|
+
"theme": {
|
|
20
|
+
"src": "examples/catpea-blog-sample-data/themes/striped-dark-solarize",
|
|
21
|
+
"dest": "examples/catpea-blog-sample-data/dist/{profile}"
|
|
22
|
+
|
|
23
|
+
},
|
|
24
|
+
|
|
8
25
|
|
|
9
26
|
"pagerizer": {
|
|
27
|
+
"pp": 24,
|
|
10
28
|
"dest": "examples/catpea-blog-sample-data/dist/{profile}"
|
|
11
29
|
},
|
|
12
30
|
|
|
@@ -20,13 +38,24 @@
|
|
|
20
38
|
"width": 1024,
|
|
21
39
|
"height": 1024,
|
|
22
40
|
"quality": 80,
|
|
23
|
-
"effort": 4
|
|
41
|
+
"effort": 4,
|
|
42
|
+
"exif": {
|
|
43
|
+
"IFD0": {
|
|
44
|
+
"Copyright": "Cat Pea",
|
|
45
|
+
"ImageDescription": "Cat Pea Blog Post Cover"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
},
|
|
25
49
|
|
|
26
50
|
"audio": {
|
|
27
|
-
"dest": "examples/catpea-blog-sample-data/dist/audio/{chapter}/docs/{id}.mp3",
|
|
28
|
-
"url": "https://catpea.github.io/chapter-
|
|
29
|
-
"preset": "balanced"
|
|
51
|
+
"dest": "examples/catpea-blog-sample-data/dist/audio/chapter-{chapter}/docs/{id}.mp3",
|
|
52
|
+
"url": "https://catpea.github.io/chapter-{chapter}/{id}.mp3",
|
|
53
|
+
"preset": "balanced",
|
|
54
|
+
"id3": {
|
|
55
|
+
"artist": "Cat Pea",
|
|
56
|
+
"album_artist": "Cat Pea",
|
|
57
|
+
"publisher": "catpea.com"
|
|
58
|
+
}
|
|
30
59
|
},
|
|
31
60
|
|
|
32
61
|
"text": {}
|
|
@@ -3,7 +3,7 @@ import { flow } from '../../index.js';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { setup, processedPosts, manifestUpdates, loadManifest, saveManifest, computeConfigHash } from './lib.js';
|
|
6
|
+
import { setup, resolvePath, processedPosts, manifestUpdates, loadManifest, saveManifest, computeConfigHash } from './lib.js';
|
|
7
7
|
|
|
8
8
|
import postScanner from './transforms/post-scanner/index.js';
|
|
9
9
|
import skipUnchanged from './transforms/skip-unchanged/index.js';
|
|
@@ -16,6 +16,7 @@ import collectPost from './transforms/collect-post/index.js';
|
|
|
16
16
|
import homepage from './transforms/homepage/index.js';
|
|
17
17
|
import pagerizer from './transforms/pagerizer/index.js';
|
|
18
18
|
import rssFeed from './transforms/rss-feed/index.js';
|
|
19
|
+
import useTheme from './transforms/use-theme/index.js';
|
|
19
20
|
|
|
20
21
|
// ─────────────────────────────────────────────
|
|
21
22
|
// Configuration
|
|
@@ -37,7 +38,7 @@ setup(baseDir, profile);
|
|
|
37
38
|
// Manifest
|
|
38
39
|
// ─────────────────────────────────────────────
|
|
39
40
|
|
|
40
|
-
const manifestPath = path.join(
|
|
41
|
+
const manifestPath = path.join(resolvePath(profile.dest), '.muriel-manifest.json');
|
|
41
42
|
const manifest = await loadManifest(manifestPath);
|
|
42
43
|
|
|
43
44
|
const configHash = computeConfigHash(profile);
|
|
@@ -58,19 +59,21 @@ console.log(`──────────────────────
|
|
|
58
59
|
|
|
59
60
|
const blog = flow([
|
|
60
61
|
|
|
61
|
-
[postScanner(profile.src), '
|
|
62
|
-
['scanned', skipUnchanged(manifest), 'post'],
|
|
62
|
+
[ postScanner({ src: profile.src }, profile.debug), skipUnchanged(manifest), 'post' ],
|
|
63
63
|
|
|
64
64
|
['post',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
|
|
66
|
+
[
|
|
67
|
+
processCover(profile.cover, profile.debug),
|
|
68
|
+
processAudio(profile.audio, profile.debug),
|
|
69
|
+
copyFiles()
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
processText(), verifyPost(), collectPost(),
|
|
73
|
+
|
|
69
74
|
'done'],
|
|
70
75
|
|
|
71
|
-
['done',
|
|
72
|
-
[homepage({ pp: 12 }), pagerizer({ pp: 24 }), rssFeed()],
|
|
73
|
-
'finished'],
|
|
76
|
+
['done', [homepage(profile.pagerizer), pagerizer(profile.pagerizer), rssFeed()], useTheme({ ...profile.theme, dest: profile.dest }), 'finished'],
|
|
74
77
|
|
|
75
78
|
], { context: { profile } });
|
|
76
79
|
|
|
@@ -37,9 +37,37 @@ export function escapeXml(str) {
|
|
|
37
37
|
.replace(/'/g, ''');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function buildPager(currentPage, totalPages, radius = 5) {
|
|
41
|
+
if (totalPages <= 1) return [];
|
|
42
|
+
|
|
43
|
+
// Small page count: list all pages descending
|
|
44
|
+
const window = radius * 2 + 1;
|
|
45
|
+
if (totalPages <= window) {
|
|
46
|
+
return Array.from({ length: totalPages }, (_, i) => {
|
|
47
|
+
const pn = totalPages - i;
|
|
48
|
+
return { text: `${pn}`, url: `page-${pn}.html`, ariaCurrent: pn === currentPage, pageNum: pn };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Large page count: circular window centered on currentPage
|
|
53
|
+
const pages = [];
|
|
54
|
+
for (let offset = -radius; offset <= radius; offset++) {
|
|
55
|
+
const pn = ((currentPage - 1 + offset + totalPages) % totalPages) + 1;
|
|
56
|
+
pages.push({ text: `${pn}`, url: `page-${pn}.html`, ariaCurrent: pn === currentPage, pageNum: pn });
|
|
57
|
+
}
|
|
58
|
+
const low = currentPage - radius;
|
|
59
|
+
const high = currentPage + radius;
|
|
60
|
+
const wrapped = pages.filter(p => p.pageNum < low || p.pageNum > high);
|
|
61
|
+
const main = pages.filter(p => p.pageNum >= low && p.pageNum <= high);
|
|
62
|
+
return [
|
|
63
|
+
...wrapped.sort((a, b) => b.pageNum - a.pageNum),
|
|
64
|
+
...main.sort((a, b) => b.pageNum - a.pageNum)
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
export function renderPostCard(post) {
|
|
41
69
|
return ` <article class="post">
|
|
42
|
-
${post.coverUrl ? `<a href="${post.permalinkUrl}"><img src="${post.coverUrl}" alt="" loading="lazy"></a>` :
|
|
70
|
+
${post.coverUrl ? `<a href="${post.permalinkUrl}"><img src="${post.coverUrl}" alt="" loading="lazy"></a>` : ``}
|
|
43
71
|
<div class="post-content">
|
|
44
72
|
<h2><a href="${post.permalinkUrl}">${post.postData.title || post.postId}</a></h2>
|
|
45
73
|
<time>${post.postData.date ? new Date(post.postData.date).toLocaleDateString() : ''}</time>
|
|
@@ -47,25 +75,6 @@ export function renderPostCard(post) {
|
|
|
47
75
|
</article>`;
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
export const pageStyles = `
|
|
51
|
-
* { box-sizing: border-box; }
|
|
52
|
-
body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 2rem; line-height: 1.6; background: #111; color: #eee; }
|
|
53
|
-
a { color: #6af; text-decoration: none; }
|
|
54
|
-
a:hover { text-decoration: underline; }
|
|
55
|
-
h1 { border-bottom: 1px solid #333; padding-bottom: 0.5rem; }
|
|
56
|
-
.posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
|
|
57
|
-
.post { background: #1a1a1a; border-radius: 8px; overflow: hidden; transition: transform 0.2s; }
|
|
58
|
-
.post:hover { transform: translateY(-4px); }
|
|
59
|
-
.post img { width: 100%; height: 180px; object-fit: cover; }
|
|
60
|
-
.post-content { padding: 1rem; }
|
|
61
|
-
.post h2 { margin: 0 0 0.5rem; font-size: 1.1rem; }
|
|
62
|
-
.post time { color: #888; font-size: 0.85rem; }
|
|
63
|
-
.pager { display: flex; justify-content: center; gap: 0.5rem; margin: 2rem 0; flex-wrap: wrap; }
|
|
64
|
-
.pager a, .pager span { padding: 0.5rem 1rem; background: #222; border-radius: 4px; }
|
|
65
|
-
.pager span[aria-current="true"] { background: #6af; color: #000; }
|
|
66
|
-
nav.nav { display: flex; justify-content: space-between; margin-bottom: 1rem; }
|
|
67
|
-
`;
|
|
68
|
-
|
|
69
78
|
export const mp3Presets = {
|
|
70
79
|
highQuality: (src, out) => [
|
|
71
80
|
'-hide_banner', '-loglevel', 'error', '-threads', '0', '-i', src,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const pagerHtml = totalPages > 1
|
|
2
|
+
? ` <nav class="pager">
|
|
3
|
+
<a href="page-${totalPages}.html">Browse Archive</a>
|
|
4
|
+
${homePager.map(p => p.ariaCurrent
|
|
5
|
+
? ` <a aria-current="true"href="${p.url}">${p.text}</a>`
|
|
6
|
+
: ` <a href="${p.url}">${p.text}</a>`
|
|
7
|
+
).join('\n')}
|
|
8
|
+
</nav>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
|
-
import { resolvePath, processedPosts, renderPostCard,
|
|
3
|
+
import { resolvePath, processedPosts, renderPostCard, buildPager, atomicWriteFile } from '../../lib.js';
|
|
4
4
|
|
|
5
5
|
export default function homepage({ pp = 12 } = {}) {
|
|
6
6
|
let expectedTotal = null;
|
|
@@ -22,33 +22,22 @@ export default function homepage({ pp = 12 } = {}) {
|
|
|
22
22
|
|
|
23
23
|
const latestPosts = sortedNewestFirst.slice(0, pp);
|
|
24
24
|
|
|
25
|
-
// Calculate archive page count for pager links
|
|
26
25
|
const archivePP = 24;
|
|
27
26
|
const totalPages = Math.ceil(validPosts.length / archivePP) || 1;
|
|
28
27
|
|
|
29
28
|
const destDir = resolvePath(profile.pagerizer.dest);
|
|
30
29
|
await mkdir(destDir, { recursive: true });
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
const pagerRadius = 5;
|
|
34
|
-
const centerPage = totalPages;
|
|
35
|
-
const allPages = Array.from(
|
|
36
|
-
{ length: pagerRadius * 2 + 1 },
|
|
37
|
-
(_, i) => {
|
|
38
|
-
let pageNum = ((centerPage - pagerRadius + i - 1 + totalPages) % totalPages) + 1;
|
|
39
|
-
return { text: `${pageNum}`, url: `page-${pageNum}.html`, pageNum };
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const lowBound = centerPage - pagerRadius;
|
|
44
|
-
const highBound = centerPage + pagerRadius;
|
|
45
|
-
const wrapped = allPages.filter(p => p.pageNum < lowBound || p.pageNum > highBound);
|
|
46
|
-
const main = allPages.filter(p => p.pageNum >= lowBound && p.pageNum <= highBound);
|
|
31
|
+
const homePager = buildPager(totalPages, totalPages);
|
|
47
32
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
const pagerHtml = totalPages > 1
|
|
34
|
+
? ` <nav class="pager">
|
|
35
|
+
${homePager.map(p => p.ariaCurrent
|
|
36
|
+
? ` <a aria-current="true"href="${p.url}">${p.text}</a>`
|
|
37
|
+
: ` <a href="${p.url}">${p.text}</a>`
|
|
38
|
+
).join('\n')}
|
|
39
|
+
</nav>`
|
|
40
|
+
: '';
|
|
52
41
|
|
|
53
42
|
const html = `<!DOCTYPE html>
|
|
54
43
|
<html lang="en">
|
|
@@ -57,24 +46,18 @@ export default function homepage({ pp = 12 } = {}) {
|
|
|
57
46
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
58
47
|
<title>${profile.title}</title>
|
|
59
48
|
<link rel="alternate" type="application/rss+xml" title="${profile.title} Feed" href="/feed.xml">
|
|
60
|
-
<style
|
|
49
|
+
<link rel="stylesheet" href="/style.css">
|
|
61
50
|
</head>
|
|
62
51
|
<body>
|
|
63
|
-
<header>
|
|
64
|
-
<h1>${profile.title}</h1>
|
|
65
|
-
</header>
|
|
66
52
|
|
|
67
53
|
<main class="posts">
|
|
68
54
|
${latestPosts.map(renderPostCard).join('\n')}
|
|
69
55
|
</main>
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
<a href="page-${totalPages}.html">Browse Archive</a>
|
|
73
|
-
${homePager.map(p => ` <a href="${p.url}">${p.text}</a>`).join('\n')}
|
|
74
|
-
</nav>
|
|
57
|
+
${pagerHtml}
|
|
75
58
|
|
|
76
59
|
<footer>
|
|
77
|
-
<p><a href="/feed.xml">RSS
|
|
60
|
+
<p><a href="/feed.xml">RSS</a></p>
|
|
78
61
|
</footer>
|
|
79
62
|
</body>
|
|
80
63
|
</html>`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
|
-
import { resolvePath, processedPosts, renderPostCard,
|
|
2
|
+
import { resolvePath, processedPosts, renderPostCard, chunk, buildPager, atomicWriteFile } from '../../lib.js';
|
|
3
3
|
|
|
4
4
|
export default function pagerizer({ pp = 24 } = {}) {
|
|
5
5
|
let expectedTotal = null;
|
|
@@ -39,30 +39,19 @@ export default function pagerizer({ pp = 24 } = {}) {
|
|
|
39
39
|
const olderPageNumber = olderChunkIndex !== null ? totalPages - olderChunkIndex : null;
|
|
40
40
|
const newerPageNumber = newerChunkIndex !== null ? totalPages - newerChunkIndex : null;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const lowBound = pageNumber - pagerRadius;
|
|
58
|
-
const highBound = pageNumber + pagerRadius;
|
|
59
|
-
const wrapped = allPages.filter(p => p.pageNum < lowBound || p.pageNum > highBound);
|
|
60
|
-
const main = allPages.filter(p => p.pageNum >= lowBound && p.pageNum <= highBound);
|
|
61
|
-
|
|
62
|
-
const pager = [
|
|
63
|
-
...wrapped.sort((a, b) => b.pageNum - a.pageNum),
|
|
64
|
-
...main.sort((a, b) => b.pageNum - a.pageNum)
|
|
65
|
-
];
|
|
42
|
+
const pager = buildPager(pageNumber, totalPages);
|
|
43
|
+
|
|
44
|
+
const pagerHtml = totalPages > 1
|
|
45
|
+
? ` <nav class="pager">
|
|
46
|
+
<a href="index.html">Home</a>
|
|
47
|
+
${pager.map(p => p.ariaCurrent
|
|
48
|
+
? ` <span aria-current="true">${p.text}</span>`
|
|
49
|
+
: ` <a href="${p.url}">${p.text}</a>`
|
|
50
|
+
).join('\n')}
|
|
51
|
+
</nav>`
|
|
52
|
+
: ` <nav class="pager">
|
|
53
|
+
<a href="index.html">Home</a>
|
|
54
|
+
</nav>`;
|
|
66
55
|
|
|
67
56
|
const html = `<!DOCTYPE html>
|
|
68
57
|
<html lang="en">
|
|
@@ -71,31 +60,23 @@ export default function pagerizer({ pp = 24 } = {}) {
|
|
|
71
60
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
72
61
|
<title>${profile.title} - Page ${pageNumber}</title>
|
|
73
62
|
<link rel="alternate" type="application/rss+xml" title="${profile.title} Feed" href="/feed.xml">
|
|
74
|
-
<style
|
|
63
|
+
<link rel="stylesheet" href="/style.css">
|
|
75
64
|
</head>
|
|
76
65
|
<body>
|
|
77
|
-
<header>
|
|
78
|
-
<h1>${profile.title}</h1>
|
|
79
|
-
</header>
|
|
80
66
|
|
|
81
|
-
<nav class="nav">
|
|
82
|
-
${newerPageNumber ? `<a href="page-${newerPageNumber}.html">← Back</a>` : '<a href="index.html">← Home</a>'}
|
|
83
|
-
<span>Page ${pageNumber} of ${totalPages}</span>
|
|
84
|
-
${olderPageNumber ? `<a href="page-${olderPageNumber}.html">Next →</a>` : '<span></span>'}
|
|
85
|
-
</nav>
|
|
86
67
|
|
|
87
68
|
<main class="posts">
|
|
88
69
|
${chunkPosts.map(renderPostCard).join('\n')}
|
|
89
70
|
</main>
|
|
90
71
|
|
|
91
|
-
<nav class="
|
|
92
|
-
<a href="index.html"
|
|
93
|
-
${
|
|
94
|
-
|
|
95
|
-
: ` <a href="${p.url}">${p.text}</a>`
|
|
96
|
-
).join('\n')}
|
|
72
|
+
<nav class="nav">
|
|
73
|
+
${newerPageNumber ? `<a href="page-${newerPageNumber}.html">← Newer</a>` : '<a href="index.html">← Home</a>'}
|
|
74
|
+
<span>Page ${pageNumber} of ${totalPages}</span>
|
|
75
|
+
${olderPageNumber ? `<a href="page-${olderPageNumber}.html">Older →</a>` : '<span></span>'}
|
|
97
76
|
</nav>
|
|
98
77
|
|
|
78
|
+
${pagerHtml}
|
|
79
|
+
|
|
99
80
|
<footer>
|
|
100
81
|
<p><a href="/feed.xml">RSS Feed</a></p>
|
|
101
82
|
</footer>
|
|
@@ -2,9 +2,10 @@ import path from 'node:path';
|
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolvePath } from '../../lib.js';
|
|
4
4
|
|
|
5
|
-
export default function postScanner(
|
|
5
|
+
export default function postScanner({src}, debug) {
|
|
6
|
+
|
|
6
7
|
return async send => {
|
|
7
|
-
const srcDir = resolvePath(
|
|
8
|
+
const srcDir = resolvePath(src);
|
|
8
9
|
console.log(`Scanning: ${srcDir}`);
|
|
9
10
|
|
|
10
11
|
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
@@ -22,13 +23,21 @@ export default function postScanner(postsDir) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
let selectedPostDirs = validPostDirs;
|
|
28
|
+
if (debug?.processOnly?.length) {
|
|
29
|
+
const allowed = new Set(debug.processOnly);
|
|
30
|
+
selectedPostDirs = validPostDirs.filter(p => allowed.has(p.postId));
|
|
31
|
+
} else if (debug?.mostRecent) {
|
|
32
|
+
selectedPostDirs = validPostDirs.slice(validPostDirs.length - debug.mostRecent);
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
const totalPosts = selectedPostDirs.length;
|
|
27
36
|
console.log(` Found ${totalPosts} posts`);
|
|
28
37
|
|
|
29
38
|
for (const { postId, postDir, dirFiles } of selectedPostDirs) {
|
|
30
|
-
const postData = JSON.parse(await readFile(path.join(postDir, 'post.json'), 'utf-8'));
|
|
31
39
|
|
|
40
|
+
const postData = JSON.parse(await readFile(path.join(postDir, 'post.json'), 'utf-8'));
|
|
32
41
|
const cover = dirFiles.find(f => f.startsWith('cover.'));
|
|
33
42
|
const audio = dirFiles.find(f => f.startsWith('audio.'));
|
|
34
43
|
|
|
@@ -5,8 +5,9 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { once } from 'node:events';
|
|
6
6
|
import { resolvePath, mp3Presets, encodingSemaphore } from '../../lib.js';
|
|
7
7
|
|
|
8
|
-
export default function processAudio(config) {
|
|
8
|
+
export default function processAudio(config, debug) {
|
|
9
9
|
const preset = config.preset || 'balanced';
|
|
10
|
+
const id3 = config.id3 || {};
|
|
10
11
|
|
|
11
12
|
return async (send, packet) => {
|
|
12
13
|
if (packet._cached) {
|
|
@@ -16,7 +17,7 @@ export default function processAudio(config) {
|
|
|
16
17
|
|
|
17
18
|
const { files, guid, postId, postData } = packet;
|
|
18
19
|
|
|
19
|
-
if (!files.audio || !fs.existsSync(files.audio)) {
|
|
20
|
+
if (!files.audio || !fs.existsSync(files.audio) || debug.skipAudio) {
|
|
20
21
|
console.log(` [audio] ${postId}: No audio file`);
|
|
21
22
|
send({ ...packet, audioResult: { skipped: true } });
|
|
22
23
|
return;
|
|
@@ -27,6 +28,23 @@ export default function processAudio(config) {
|
|
|
27
28
|
.replace('{chapter}', postData.chapter || '0')
|
|
28
29
|
.replace('{id}', postData.id)
|
|
29
30
|
);
|
|
31
|
+
|
|
32
|
+
// If output already exists, skip encoding (delete file to force rebuild)
|
|
33
|
+
if (fs.existsSync(destPath)) {
|
|
34
|
+
const outputStats = fs.statSync(destPath);
|
|
35
|
+
console.log(` [audio] ${postId}: exists ${(outputStats.size / 1024 / 1024).toFixed(1)}MB`);
|
|
36
|
+
send({
|
|
37
|
+
...packet,
|
|
38
|
+
audioResult: {
|
|
39
|
+
success: true,
|
|
40
|
+
path: destPath,
|
|
41
|
+
url: config.url.replace('{chapter}', postData.chapter || '0').replace('{id}', postData.id),
|
|
42
|
+
size: outputStats.size
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
await mkdir(path.dirname(destPath), { recursive: true });
|
|
31
49
|
|
|
32
50
|
const presetFn = mp3Presets[preset];
|
|
@@ -40,6 +58,11 @@ export default function processAudio(config) {
|
|
|
40
58
|
const tmpPath = destPath + '.tmp';
|
|
41
59
|
const args = presetFn(files.audio, tmpPath);
|
|
42
60
|
|
|
61
|
+
// Append id3 metadata flags
|
|
62
|
+
for (const [key, value] of Object.entries(id3)) {
|
|
63
|
+
args.splice(args.indexOf('-f'), 0, '-metadata', `${key}=${value}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
await encodingSemaphore.acquire();
|
|
44
67
|
let code, stderr = '';
|
|
45
68
|
try {
|
|
@@ -67,7 +90,7 @@ export default function processAudio(config) {
|
|
|
67
90
|
audioResult: {
|
|
68
91
|
success: true,
|
|
69
92
|
path: destPath,
|
|
70
|
-
url: config.url.replace('{id}', postData.id),
|
|
93
|
+
url: config.url.replace('{chapter}', postData.chapter || '0').replace('{id}', postData.id),
|
|
71
94
|
size: outputStats.size,
|
|
72
95
|
reduction: parseFloat(reduction)
|
|
73
96
|
}
|
|
@@ -2,12 +2,12 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { mkdir, rename } from 'node:fs/promises';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
-
import { resolvePath, encodingSemaphore } from '../../lib.js';
|
|
5
|
+
import { resolvePath, encodingSemaphore, atomicCopyFile } from '../../lib.js';
|
|
6
6
|
|
|
7
7
|
sharp.concurrency(1);
|
|
8
8
|
|
|
9
|
-
export default function processCover(config) {
|
|
10
|
-
const { width = 1024, height = 1024, quality = 80, effort = 4 } = config;
|
|
9
|
+
export default function processCover(config, debug) {
|
|
10
|
+
const { width = 1024, height = 1024, quality = 80, effort = 4, exif = {} } = config;
|
|
11
11
|
|
|
12
12
|
return async (send, packet) => {
|
|
13
13
|
if (packet._cached) {
|
|
@@ -17,27 +17,59 @@ export default function processCover(config) {
|
|
|
17
17
|
|
|
18
18
|
const { files, guid, postId } = packet;
|
|
19
19
|
|
|
20
|
-
if (!files.cover || !fs.existsSync(files.cover)) {
|
|
20
|
+
if (!files.cover || !fs.existsSync(files.cover) || debug.skipCovers) {
|
|
21
21
|
console.log(` [cover] ${postId}: No cover image`);
|
|
22
22
|
send({ ...packet, coverResult: { skipped: true } });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const destPath = resolvePath(config.dest.replace('{guid}', guid));
|
|
27
|
+
|
|
28
|
+
// If output already exists, skip encoding (delete file to force rebuild)
|
|
29
|
+
if (fs.existsSync(destPath)) {
|
|
30
|
+
const stats = fs.statSync(destPath);
|
|
31
|
+
console.log(` [cover] ${postId}: exists ${(stats.size / 1024).toFixed(1)}KB`);
|
|
32
|
+
send({
|
|
33
|
+
...packet,
|
|
34
|
+
coverResult: {
|
|
35
|
+
success: true,
|
|
36
|
+
path: destPath,
|
|
37
|
+
url: config.url.replace('{guid}', guid),
|
|
38
|
+
size: stats.size
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
27
44
|
await mkdir(path.dirname(destPath), { recursive: true });
|
|
28
45
|
|
|
29
46
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (files.cover.endsWith('.avif')) {
|
|
48
|
+
await atomicCopyFile(files.cover, destPath);
|
|
49
|
+
console.log(` [cover] ${postId}: copied (already AVIF)`);
|
|
50
|
+
} else {
|
|
51
|
+
const tmpPath = destPath + '.tmp';
|
|
52
|
+
await encodingSemaphore.acquire();
|
|
53
|
+
try {
|
|
54
|
+
let pipeline = sharp(files.cover)
|
|
55
|
+
.resize(width, height, { kernel: sharp.kernel.mitchell, fit: 'inside' })
|
|
56
|
+
.avif({ quality, effort });
|
|
57
|
+
if (Object.keys(exif).length > 0) {
|
|
58
|
+
pipeline = pipeline.withExif(exif);
|
|
59
|
+
}
|
|
60
|
+
await pipeline.toFile(tmpPath);
|
|
61
|
+
} catch (sharpErr) {
|
|
62
|
+
if (sharpErr.message.includes('unsupported image format')) {
|
|
63
|
+
console.log(` [cover] ${postId}: sharp can't decode, copying as-is`);
|
|
64
|
+
await atomicCopyFile(files.cover, destPath);
|
|
65
|
+
} else {
|
|
66
|
+
throw sharpErr;
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
encodingSemaphore.release();
|
|
70
|
+
}
|
|
71
|
+
if (fs.existsSync(tmpPath)) await rename(tmpPath, destPath);
|
|
39
72
|
}
|
|
40
|
-
await rename(tmpPath, destPath);
|
|
41
73
|
|
|
42
74
|
const stats = fs.statSync(destPath);
|
|
43
75
|
console.log(` [cover] ${postId}: ${(stats.size / 1024).toFixed(1)}KB → ${path.basename(destPath)}`);
|
|
@@ -7,6 +7,7 @@ import { resolvePath, manifestUpdates, hashFileContent } from '../../lib.js';
|
|
|
7
7
|
export default function skipUnchanged(manifest) {
|
|
8
8
|
|
|
9
9
|
return async (send, packet) => {
|
|
10
|
+
|
|
10
11
|
const { postId, postDir, guid, profile } = packet;
|
|
11
12
|
const entry = manifest.posts[postId];
|
|
12
13
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdir, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { resolvePath, atomicCopyFile } from '../../lib.js';
|
|
4
|
+
|
|
5
|
+
export default function useTheme(config) {
|
|
6
|
+
const themeSrc = resolvePath(config.src);
|
|
7
|
+
const destDir = resolvePath(config.dest);
|
|
8
|
+
|
|
9
|
+
return async (send, packet) => {
|
|
10
|
+
const allComplete = packet.branches?.every(b => b._complete);
|
|
11
|
+
if (!allComplete) {
|
|
12
|
+
send(packet);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const count = await copyRecursive(themeSrc, destDir);
|
|
18
|
+
console.log(` [theme] Installed ${count} file(s) from ${path.basename(themeSrc)}`);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(` [theme] Error - ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
send(packet);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function copyRecursive(src, dest) {
|
|
28
|
+
await mkdir(dest, { recursive: true });
|
|
29
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const srcPath = path.join(src, entry.name);
|
|
33
|
+
const destPath = path.join(dest, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
count += await copyRecursive(srcPath, destPath);
|
|
36
|
+
} else {
|
|
37
|
+
await atomicCopyFile(srcPath, destPath);
|
|
38
|
+
count++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
package/index.js
CHANGED
|
@@ -27,6 +27,19 @@ function createPipe(name) {
|
|
|
27
27
|
* Worker pool (optional)
|
|
28
28
|
* ───────────────────────────────────────────── */
|
|
29
29
|
|
|
30
|
+
/*
|
|
31
|
+
NOTE: About createWorkerPool
|
|
32
|
+
You do need to pass a size explicitly, but it doesn't have to be CPU count. Here's how it works:
|
|
33
|
+
- Activation: Pass workers in options — flow([...], { workers: 4 }) or flow([...], { workers: os.cpus().length }). If omitted, size is undefined and it returns null (disabled).
|
|
34
|
+
- Mechanism: Creates size Worker threads, distributes work round-robin via cursor++ % workers.length.
|
|
35
|
+
- Serialization: The filter function is sent as filter.toString() and eval()'d inside the worker. The packet is passed via postMessage (structured clone).
|
|
36
|
+
Important limitation: Because filters are serialized via toString() + eval(), the worker pool only works with pure, self-contained transforms — functions that don't import external modules or close over external variables. A function
|
|
37
|
+
like (send, packet) => { send({ ...packet, x: packet.y * 2 }) } works fine. But your blog builder transforms (importing sharp, ffmpeg, fs, etc.) would not work with it.
|
|
38
|
+
For the blog builder, the encodingSemaphore is the right approach. The heavy work (sharp, ffmpeg) happens in external native processes anyway — worker threads wouldn't add anything. The semaphore limits concurrency to os.cpus().length
|
|
39
|
+
without the serialization constraint.
|
|
40
|
+
The worker pool is more suited for CPU-bound pure-JS transforms where you want to avoid blocking the main thread — e.g., heavy string processing, JSON transformations, or math-intensive filters across thousands of packets.
|
|
41
|
+
*/
|
|
42
|
+
|
|
30
43
|
function createWorkerPool(size) {
|
|
31
44
|
if (size === undefined) return null;
|
|
32
45
|
|
|
@@ -207,11 +220,20 @@ export function flow(definition, options = {}) {
|
|
|
207
220
|
|
|
208
221
|
// First pass: set up all edges (listeners)
|
|
209
222
|
for (const edge of definition) {
|
|
210
|
-
// Producer → pipe (defer execution)
|
|
223
|
+
// Producer → [stages] → pipe (defer execution)
|
|
211
224
|
if (typeof edge[0] === 'function') {
|
|
212
225
|
const producer = edge[0];
|
|
213
|
-
const
|
|
214
|
-
|
|
226
|
+
const outputName = edge[edge.length - 1];
|
|
227
|
+
const stages = compileStages(edge.slice(1, -1));
|
|
228
|
+
|
|
229
|
+
if (stages.length === 0) {
|
|
230
|
+
const pipe = getPipe(outputName);
|
|
231
|
+
producers.push(() => producer(packet => pipe.send({ ...packet, ...ctx })));
|
|
232
|
+
} else {
|
|
233
|
+
const producerPipe = createPipe(`_producer_${producers.length}`);
|
|
234
|
+
connectEdge(producerPipe, stages, getPipe(outputName), ctx, pool);
|
|
235
|
+
producers.push(() => producer(packet => producerPipe.send({ ...packet, ...ctx })));
|
|
236
|
+
}
|
|
215
237
|
continue;
|
|
216
238
|
}
|
|
217
239
|
|
package/package.json
CHANGED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"configHash": "4334d284663296411f31a0421e4d6b6c0511ee0430a1a9db86e1cfa2872cd911",
|
|
4
|
-
"posts": {
|
|
5
|
-
"poem-1001": {
|
|
6
|
-
"compositeHash": "6715a83bc29b60037c7cf96e946a86d2b62f551d06e8e75da3f66eed281c4513",
|
|
7
|
-
"files": {
|
|
8
|
-
"audio.mp3": {
|
|
9
|
-
"mtime": 1770321388663.5708,
|
|
10
|
-
"size": 3947444,
|
|
11
|
-
"hash": "1ac432bdc39fd1fd56e60db15e6c09b7b12d2014e5e646c1757b83901783abec"
|
|
12
|
-
},
|
|
13
|
-
"cover.jpg": {
|
|
14
|
-
"mtime": 1770321388663.7854,
|
|
15
|
-
"size": 541318,
|
|
16
|
-
"hash": "d33e976401350485b33b744580cff2039defccc9fa3c4e06cf91f7d2163a1d48"
|
|
17
|
-
},
|
|
18
|
-
"post.json": {
|
|
19
|
-
"mtime": 1770321388663.9695,
|
|
20
|
-
"size": 311,
|
|
21
|
-
"hash": "25686c8858fa7350f8b2d5f72b03c64eaefd3e4e8f1c6f37350e3055c121a80c"
|
|
22
|
-
},
|
|
23
|
-
"text.md": {
|
|
24
|
-
"mtime": 1770321388663.9695,
|
|
25
|
-
"size": 1283,
|
|
26
|
-
"hash": "ff971ec162528681d1ab586f50b3ec194385809d368dae2194e2c5e9a5e25cd6"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"results": {
|
|
30
|
-
"coverResult": {
|
|
31
|
-
"success": true,
|
|
32
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/d0ee9d67-040d-41b6-8ee6-0913cd195b2d/cover.avif",
|
|
33
|
-
"url": "/permalink/d0ee9d67-040d-41b6-8ee6-0913cd195b2d/cover.avif",
|
|
34
|
-
"size": 109011
|
|
35
|
-
},
|
|
36
|
-
"audioResult": {
|
|
37
|
-
"success": true,
|
|
38
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/audio/5/docs/poem-1001.mp3",
|
|
39
|
-
"url": "https://catpea.github.io/chapter-15/poem-1001.mp3",
|
|
40
|
-
"size": 1325895,
|
|
41
|
-
"reduction": 66.4
|
|
42
|
-
},
|
|
43
|
-
"textResult": {
|
|
44
|
-
"success": true,
|
|
45
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/d0ee9d67-040d-41b6-8ee6-0913cd195b2d/index.html",
|
|
46
|
-
"htmlLength": 2238
|
|
47
|
-
},
|
|
48
|
-
"filesResult": {
|
|
49
|
-
"skipped": true,
|
|
50
|
-
"reason": "no files dir"
|
|
51
|
-
},
|
|
52
|
-
"valid": true,
|
|
53
|
-
"errors": [],
|
|
54
|
-
"collectedPost": {
|
|
55
|
-
"postId": "poem-1001",
|
|
56
|
-
"guid": "d0ee9d67-040d-41b6-8ee6-0913cd195b2d",
|
|
57
|
-
"valid": true,
|
|
58
|
-
"errors": [],
|
|
59
|
-
"postData": {
|
|
60
|
-
"id": "poem-1001",
|
|
61
|
-
"guid": "d0ee9d67-040d-41b6-8ee6-0913cd195b2d",
|
|
62
|
-
"chapter": 5,
|
|
63
|
-
"title": "Little By Little; Or, To Live Above The Common Levels Of Life",
|
|
64
|
-
"description": null,
|
|
65
|
-
"date": "2022-11-27T04:25:38.628Z",
|
|
66
|
-
"lastmod": null,
|
|
67
|
-
"artwork": [
|
|
68
|
-
"https://unsplash.com/photos/TvpTk9m5H1E"
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
"coverUrl": "/permalink/d0ee9d67-040d-41b6-8ee6-0913cd195b2d/cover.avif",
|
|
72
|
-
"audioUrl": "https://catpea.github.io/chapter-15/poem-1001.mp3",
|
|
73
|
-
"permalinkUrl": "/permalink/d0ee9d67-040d-41b6-8ee6-0913cd195b2d/"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
"poem-1004": {
|
|
78
|
-
"compositeHash": "0e094d783dd770333fc2ad7315e6971b3ff2925e7895555025b62a619b094f40",
|
|
79
|
-
"files": {
|
|
80
|
-
"audio.mp3": {
|
|
81
|
-
"mtime": 1770321388671.8887,
|
|
82
|
-
"size": 2972368,
|
|
83
|
-
"hash": "2c279e07095e0c1cf88dcfa82220e4976ba76380e747a66851f878f9fdf5b952"
|
|
84
|
-
},
|
|
85
|
-
"cover.jpg": {
|
|
86
|
-
"mtime": 1770321388672.2324,
|
|
87
|
-
"size": 310641,
|
|
88
|
-
"hash": "3d78f4cce735ec0cd2d4acff69cfaa070c1b2a941370781edad791017492cf51"
|
|
89
|
-
},
|
|
90
|
-
"post.json": {
|
|
91
|
-
"mtime": 1770321388672.7017,
|
|
92
|
-
"size": 353,
|
|
93
|
-
"hash": "69de9df29ae2f8a3c577dab12cae142d0317d44abfc4e7e2131de1a819786223"
|
|
94
|
-
},
|
|
95
|
-
"text.md": {
|
|
96
|
-
"mtime": 1770321388672.7017,
|
|
97
|
-
"size": 1862,
|
|
98
|
-
"hash": "6cf8f0ad86dc0299aaab5d7f0c261da9606661fb93e086bb85b814bd6545cdfa"
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
"results": {
|
|
102
|
-
"coverResult": {
|
|
103
|
-
"success": true,
|
|
104
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/17f655e8-1a02-4040-94da-7e9ff2da9bb1/cover.avif",
|
|
105
|
-
"url": "/permalink/17f655e8-1a02-4040-94da-7e9ff2da9bb1/cover.avif",
|
|
106
|
-
"size": 274128
|
|
107
|
-
},
|
|
108
|
-
"audioResult": {
|
|
109
|
-
"success": true,
|
|
110
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/audio/5/docs/poem-1004.mp3",
|
|
111
|
-
"url": "https://catpea.github.io/chapter-15/poem-1004.mp3",
|
|
112
|
-
"size": 1043753,
|
|
113
|
-
"reduction": 64.9
|
|
114
|
-
},
|
|
115
|
-
"textResult": {
|
|
116
|
-
"success": true,
|
|
117
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/17f655e8-1a02-4040-94da-7e9ff2da9bb1/index.html",
|
|
118
|
-
"htmlLength": 2898
|
|
119
|
-
},
|
|
120
|
-
"filesResult": {
|
|
121
|
-
"skipped": true,
|
|
122
|
-
"reason": "no files dir"
|
|
123
|
-
},
|
|
124
|
-
"valid": true,
|
|
125
|
-
"errors": [],
|
|
126
|
-
"collectedPost": {
|
|
127
|
-
"postId": "poem-1004",
|
|
128
|
-
"guid": "17f655e8-1a02-4040-94da-7e9ff2da9bb1",
|
|
129
|
-
"valid": true,
|
|
130
|
-
"errors": [],
|
|
131
|
-
"postData": {
|
|
132
|
-
"id": "poem-1004",
|
|
133
|
-
"guid": "17f655e8-1a02-4040-94da-7e9ff2da9bb1",
|
|
134
|
-
"chapter": 5,
|
|
135
|
-
"title": "Confusing Programming Can Be Pretty Colorful If You Build Everything Out Of Interesting Little Machines",
|
|
136
|
-
"description": null,
|
|
137
|
-
"date": "2022-11-30T04:11:09.289Z",
|
|
138
|
-
"lastmod": null,
|
|
139
|
-
"artwork": [
|
|
140
|
-
"https://unsplash.com/photos/vuAkRNH4sGA"
|
|
141
|
-
]
|
|
142
|
-
},
|
|
143
|
-
"coverUrl": "/permalink/17f655e8-1a02-4040-94da-7e9ff2da9bb1/cover.avif",
|
|
144
|
-
"audioUrl": "https://catpea.github.io/chapter-15/poem-1004.mp3",
|
|
145
|
-
"permalinkUrl": "/permalink/17f655e8-1a02-4040-94da-7e9ff2da9bb1/"
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
"poem-1005": {
|
|
150
|
-
"compositeHash": "9e61d9adcdbf0f511eaf5cc44810e274e7cc57f7d99c389b095732677296f0f9",
|
|
151
|
-
"files": {
|
|
152
|
-
"audio.mp3": {
|
|
153
|
-
"mtime": 1770321388674.134,
|
|
154
|
-
"size": 7287177,
|
|
155
|
-
"hash": "861bbcc081f4a378b86184cffe4bfe7632be7ca724b84d6242774666ef51add1"
|
|
156
|
-
},
|
|
157
|
-
"cover.jpg": {
|
|
158
|
-
"mtime": 1770321388674.279,
|
|
159
|
-
"size": 509184,
|
|
160
|
-
"hash": "5e4317a39b4855cbb144fdc599dc71795770779d1f706525cb14525e479a5fb4"
|
|
161
|
-
},
|
|
162
|
-
"post.json": {
|
|
163
|
-
"mtime": 1770321388674.4119,
|
|
164
|
-
"size": 320,
|
|
165
|
-
"hash": "093c9a24e6a7b9a654958fa0b8ede2b6d8be75114336909a2cd2061146626eb7"
|
|
166
|
-
},
|
|
167
|
-
"text.md": {
|
|
168
|
-
"mtime": 1770321388674.4119,
|
|
169
|
-
"size": 2540,
|
|
170
|
-
"hash": "54588e268459278b034c697f2b29399eff2b8f1b82e0b080b80ab1e4044e7a2e"
|
|
171
|
-
}
|
|
172
|
-
},
|
|
173
|
-
"results": {
|
|
174
|
-
"coverResult": {
|
|
175
|
-
"success": true,
|
|
176
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/3c110103-9606-43fe-ba72-75a5045f8a0a/cover.avif",
|
|
177
|
-
"url": "/permalink/3c110103-9606-43fe-ba72-75a5045f8a0a/cover.avif",
|
|
178
|
-
"size": 54218
|
|
179
|
-
},
|
|
180
|
-
"audioResult": {
|
|
181
|
-
"success": true,
|
|
182
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/audio/5/docs/poem-1005.mp3",
|
|
183
|
-
"url": "https://catpea.github.io/chapter-15/poem-1005.mp3",
|
|
184
|
-
"size": 2555933,
|
|
185
|
-
"reduction": 64.9
|
|
186
|
-
},
|
|
187
|
-
"textResult": {
|
|
188
|
-
"success": true,
|
|
189
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/3c110103-9606-43fe-ba72-75a5045f8a0a/index.html",
|
|
190
|
-
"htmlLength": 3569
|
|
191
|
-
},
|
|
192
|
-
"filesResult": {
|
|
193
|
-
"skipped": true,
|
|
194
|
-
"reason": "no files dir"
|
|
195
|
-
},
|
|
196
|
-
"valid": true,
|
|
197
|
-
"errors": [],
|
|
198
|
-
"collectedPost": {
|
|
199
|
-
"postId": "poem-1005",
|
|
200
|
-
"guid": "3c110103-9606-43fe-ba72-75a5045f8a0a",
|
|
201
|
-
"valid": true,
|
|
202
|
-
"errors": [],
|
|
203
|
-
"postData": {
|
|
204
|
-
"id": "poem-1005",
|
|
205
|
-
"guid": "3c110103-9606-43fe-ba72-75a5045f8a0a",
|
|
206
|
-
"chapter": 5,
|
|
207
|
-
"title": "Prototyping And Coding Your First Web Operating System and Web Desktop",
|
|
208
|
-
"description": null,
|
|
209
|
-
"date": "2022-12-01T04:06:47.823Z",
|
|
210
|
-
"lastmod": null,
|
|
211
|
-
"artwork": [
|
|
212
|
-
"https://unsplash.com/photos/64YrPKiguAE"
|
|
213
|
-
]
|
|
214
|
-
},
|
|
215
|
-
"coverUrl": "/permalink/3c110103-9606-43fe-ba72-75a5045f8a0a/cover.avif",
|
|
216
|
-
"audioUrl": "https://catpea.github.io/chapter-15/poem-1005.mp3",
|
|
217
|
-
"permalinkUrl": "/permalink/3c110103-9606-43fe-ba72-75a5045f8a0a/"
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
"poem-1003": {
|
|
222
|
-
"compositeHash": "e41d8865afd9bc0dd3d41dea6d944d56c62f8ad5f897392973521babc82ab6a0",
|
|
223
|
-
"files": {
|
|
224
|
-
"audio.mp3": {
|
|
225
|
-
"mtime": 1770321388668.4753,
|
|
226
|
-
"size": 8635341,
|
|
227
|
-
"hash": "0fd28140c8a085308635b72332a3994665805898a0f904acad7f560d0ac1f93c"
|
|
228
|
-
},
|
|
229
|
-
"cover.jpg": {
|
|
230
|
-
"mtime": 1770321388668.6865,
|
|
231
|
-
"size": 215096,
|
|
232
|
-
"hash": "37c0d2e353f85f436a877af2856b88fa1e17875efe8e13d9e7e08fb338720e24"
|
|
233
|
-
},
|
|
234
|
-
"post.json": {
|
|
235
|
-
"mtime": 1770321388668.875,
|
|
236
|
-
"size": 277,
|
|
237
|
-
"hash": "3f72a82af449174047a1338f5cd80e45f8b4da70a48e16370c6c7ac4191b2a17"
|
|
238
|
-
},
|
|
239
|
-
"text.md": {
|
|
240
|
-
"mtime": 1770321388668.875,
|
|
241
|
-
"size": 3545,
|
|
242
|
-
"hash": "acdf38771363c2eb5bcd6967d97d600b79ff2787d5efa59546f46ff39850dd36"
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
"results": {
|
|
246
|
-
"coverResult": {
|
|
247
|
-
"success": true,
|
|
248
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/7c262035-1b1e-4b9c-ac3e-c1f9b2583538/cover.avif",
|
|
249
|
-
"url": "/permalink/7c262035-1b1e-4b9c-ac3e-c1f9b2583538/cover.avif",
|
|
250
|
-
"size": 104642
|
|
251
|
-
},
|
|
252
|
-
"audioResult": {
|
|
253
|
-
"success": true,
|
|
254
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/audio/5/docs/poem-1003.mp3",
|
|
255
|
-
"url": "https://catpea.github.io/chapter-15/poem-1003.mp3",
|
|
256
|
-
"size": 2986536,
|
|
257
|
-
"reduction": 65.4
|
|
258
|
-
},
|
|
259
|
-
"textResult": {
|
|
260
|
-
"success": true,
|
|
261
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/7c262035-1b1e-4b9c-ac3e-c1f9b2583538/index.html",
|
|
262
|
-
"htmlLength": 4501
|
|
263
|
-
},
|
|
264
|
-
"filesResult": {
|
|
265
|
-
"skipped": true,
|
|
266
|
-
"reason": "no files dir"
|
|
267
|
-
},
|
|
268
|
-
"valid": true,
|
|
269
|
-
"errors": [],
|
|
270
|
-
"collectedPost": {
|
|
271
|
-
"postId": "poem-1003",
|
|
272
|
-
"guid": "7c262035-1b1e-4b9c-ac3e-c1f9b2583538",
|
|
273
|
-
"valid": true,
|
|
274
|
-
"errors": [],
|
|
275
|
-
"postData": {
|
|
276
|
-
"id": "poem-1003",
|
|
277
|
-
"guid": "7c262035-1b1e-4b9c-ac3e-c1f9b2583538",
|
|
278
|
-
"chapter": 5,
|
|
279
|
-
"title": "You Must Unlock Your Genius",
|
|
280
|
-
"description": null,
|
|
281
|
-
"date": "2022-11-29T04:28:54.843Z",
|
|
282
|
-
"lastmod": null,
|
|
283
|
-
"artwork": [
|
|
284
|
-
"https://unsplash.com/photos/6EdFFNU4Qlw"
|
|
285
|
-
]
|
|
286
|
-
},
|
|
287
|
-
"coverUrl": "/permalink/7c262035-1b1e-4b9c-ac3e-c1f9b2583538/cover.avif",
|
|
288
|
-
"audioUrl": "https://catpea.github.io/chapter-15/poem-1003.mp3",
|
|
289
|
-
"permalinkUrl": "/permalink/7c262035-1b1e-4b9c-ac3e-c1f9b2583538/"
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
|
-
"poem-1002": {
|
|
294
|
-
"compositeHash": "282c701c825d371d946258a0b1bf9dbf77057fdcdc377cd2ee8d1dcd4954173e",
|
|
295
|
-
"files": {
|
|
296
|
-
"audio.mp3": {
|
|
297
|
-
"mtime": 1770321388665.9543,
|
|
298
|
-
"size": 24097197,
|
|
299
|
-
"hash": "e32492dddd818331cabbd7d5e2f8eda3d780a92d04cf6dfe99d05727440030a7"
|
|
300
|
-
},
|
|
301
|
-
"cover.jpg": {
|
|
302
|
-
"mtime": 1770321388666.1982,
|
|
303
|
-
"size": 327475,
|
|
304
|
-
"hash": "46d2aad492fa446624bf30ed89f56f888ea79ca682e2686bb7b1b27b6942e414"
|
|
305
|
-
},
|
|
306
|
-
"post.json": {
|
|
307
|
-
"mtime": 1770321388666.4639,
|
|
308
|
-
"size": 315,
|
|
309
|
-
"hash": "f2a340e7ce7b23b5ccfe53cf4cc25a98886ac8ecc2e9839ca704386869a6ab72"
|
|
310
|
-
},
|
|
311
|
-
"text.md": {
|
|
312
|
-
"mtime": 1770321388666.4639,
|
|
313
|
-
"size": 2924,
|
|
314
|
-
"hash": "545f1a5b98b1a5b4267198c14b6b3becdb4c1ea662a5fcc794d0745e9999325b"
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
"results": {
|
|
318
|
-
"coverResult": {
|
|
319
|
-
"success": true,
|
|
320
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/93be5330-7fc4-4bd2-b7e4-1d5efc024cca/cover.avif",
|
|
321
|
-
"url": "/permalink/93be5330-7fc4-4bd2-b7e4-1d5efc024cca/cover.avif",
|
|
322
|
-
"size": 185760
|
|
323
|
-
},
|
|
324
|
-
"audioResult": {
|
|
325
|
-
"success": true,
|
|
326
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/audio/5/docs/poem-1002.mp3",
|
|
327
|
-
"url": "https://catpea.github.io/chapter-15/poem-1002.mp3",
|
|
328
|
-
"size": 8285538,
|
|
329
|
-
"reduction": 65.6
|
|
330
|
-
},
|
|
331
|
-
"textResult": {
|
|
332
|
-
"success": true,
|
|
333
|
-
"path": "/home/meow/Universe/Development/npm/muriel/examples/catpea-blog-sample-data/dist/catpea_www/permalink/93be5330-7fc4-4bd2-b7e4-1d5efc024cca/index.html",
|
|
334
|
-
"htmlLength": 4023
|
|
335
|
-
},
|
|
336
|
-
"filesResult": {
|
|
337
|
-
"skipped": true,
|
|
338
|
-
"reason": "no files dir"
|
|
339
|
-
},
|
|
340
|
-
"valid": true,
|
|
341
|
-
"errors": [],
|
|
342
|
-
"collectedPost": {
|
|
343
|
-
"postId": "poem-1002",
|
|
344
|
-
"guid": "93be5330-7fc4-4bd2-b7e4-1d5efc024cca",
|
|
345
|
-
"valid": true,
|
|
346
|
-
"errors": [],
|
|
347
|
-
"postData": {
|
|
348
|
-
"id": "poem-1002",
|
|
349
|
-
"guid": "93be5330-7fc4-4bd2-b7e4-1d5efc024cca",
|
|
350
|
-
"chapter": 5,
|
|
351
|
-
"title": "We Are Star Babies; Or, The World Needs You To Unlock Your Genius",
|
|
352
|
-
"description": null,
|
|
353
|
-
"date": "2022-11-28T00:43:31.853Z",
|
|
354
|
-
"lastmod": null,
|
|
355
|
-
"artwork": [
|
|
356
|
-
"https://unsplash.com/photos/AmkpzghJ5eo"
|
|
357
|
-
]
|
|
358
|
-
},
|
|
359
|
-
"coverUrl": "/permalink/93be5330-7fc4-4bd2-b7e4-1d5efc024cca/cover.avif",
|
|
360
|
-
"audioUrl": "https://catpea.github.io/chapter-15/poem-1002.mp3",
|
|
361
|
-
"permalinkUrl": "/permalink/93be5330-7fc4-4bd2-b7e4-1d5efc024cca/"
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|