playlist-admin 2.6.77 → 2.6.79
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/cli/lib/lufs.js +2 -0
- package/cli/lib/pipeline.js +4 -1
- package/cli/lib/playlist_parser.js +6 -2
- package/cli/lib/podcast.js +19 -5
- package/cli/lib/scraper.js +3 -1
- package/package.json +2 -2
package/cli/lib/lufs.js
CHANGED
|
@@ -24,6 +24,8 @@ export class LufsService {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
_resolvePlaylistPath(entry, playlistsPath, basePath, libraryPath) {
|
|
27
|
+
let decodePath = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
28
|
+
entry = decodePath(entry);
|
|
27
29
|
if (!entry) return null;
|
|
28
30
|
if (!entry.includes('.') && !entry.includes('\\') && !entry.includes('/')) return null;
|
|
29
31
|
const resolved = path.join(playlistsPath, entry);
|
package/cli/lib/pipeline.js
CHANGED
|
@@ -269,6 +269,8 @@ export class PipelineOrchestrator {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
_trackFileExists(entry) {
|
|
272
|
+
let decodePath = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
273
|
+
entry = decodePath(entry);
|
|
272
274
|
if (!entry.includes('.') && !entry.includes('\\')) return true;
|
|
273
275
|
const relToPl = path.join(this.config.playlistsPath, entry);
|
|
274
276
|
if (fs.existsSync(relToPl)) return true;
|
|
@@ -334,7 +336,8 @@ export class PipelineOrchestrator {
|
|
|
334
336
|
for (const filePath of newUnsorted) {
|
|
335
337
|
const absFilePath = path.resolve(filePath);
|
|
336
338
|
sb += `#EXTINF:-1,${stemOf(filePath)}\n`;
|
|
337
|
-
|
|
339
|
+
// Echo Nightly compatible: URI-encode the relative path
|
|
340
|
+
sb += `${encodeURIComponent(relativePath(absFilePath, absPlPath)).replace(/%2F/g, '/')}\n`;
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
fs.writeFileSync(unsortedPath, sb, { flag: existingStems.size === 0 ? 'w' : 'a', encoding: 'utf-8' });
|
|
@@ -2,6 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { stemOf } from './util.js';
|
|
3
3
|
|
|
4
4
|
// Port of lib/services/playlist_parser.dart
|
|
5
|
+
function decodePath(s) {
|
|
6
|
+
try { return decodeURIComponent(s); } catch { return s; }
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
export class PlaylistParser {
|
|
6
10
|
static isInternalPlaylist(name) {
|
|
7
11
|
const n = name.toLowerCase();
|
|
@@ -31,7 +35,7 @@ export class PlaylistParser {
|
|
|
31
35
|
const name = pathLine.includes('\\') || pathLine.includes('/')
|
|
32
36
|
? pathLine.replace(/[\\/]/g, '\\').split('\\').pop()
|
|
33
37
|
: pathLine;
|
|
34
|
-
songs.push(name.replace(/\.[^.]+$/, ''));
|
|
38
|
+
songs.push(decodePath(name).replace(/\.[^.]+$/, ''));
|
|
35
39
|
i = j + 1;
|
|
36
40
|
} else {
|
|
37
41
|
i++;
|
|
@@ -64,7 +68,7 @@ export class PlaylistParser {
|
|
|
64
68
|
let j = i + 1;
|
|
65
69
|
while (j < lines.length && (lines[j].startsWith('#') || lines[j].length === 0)) j++;
|
|
66
70
|
if (j < lines.length) {
|
|
67
|
-
entries.push(lines[j]);
|
|
71
|
+
entries.push(decodePath(lines[j]));
|
|
68
72
|
i = j + 1;
|
|
69
73
|
} else {
|
|
70
74
|
i++;
|
package/cli/lib/podcast.js
CHANGED
|
@@ -16,14 +16,22 @@ function parseRss(xml) {
|
|
|
16
16
|
const block = m[1];
|
|
17
17
|
const title = (block.match(/<title>([\s\S]*?)<\/title>/) || [])[1] || '';
|
|
18
18
|
const pubDate = (block.match(/<pubDate>([\s\S]*?)<\/pubDate>/) || [])[1] || '';
|
|
19
|
-
const enclosure = block.match(/<enclosure[^>]
|
|
19
|
+
const enclosure = block.match(/<enclosure[^>]*\/?>/);
|
|
20
|
+
let audioUrl = '';
|
|
21
|
+
let audioType = '';
|
|
22
|
+
if (enclosure) {
|
|
23
|
+
const u = enclosure[0].match(/url="\s*([^"]+)"/);
|
|
24
|
+
const t = enclosure[0].match(/type="([^"]+)"/);
|
|
25
|
+
if (u) audioUrl = decodeXml(u[1]);
|
|
26
|
+
if (t) audioType = t[1];
|
|
27
|
+
}
|
|
20
28
|
const duration = (block.match(/<itunes:duration>([\s\S]*?)<\/itunes:duration>/) || [])[1] || '';
|
|
21
29
|
const description = (block.match(/<description>([\s\S]*?)<\/description>/) || [])[1] || '';
|
|
22
30
|
items.push({
|
|
23
31
|
title: decodeXml(title),
|
|
24
32
|
pub_date: decodeXml(pubDate),
|
|
25
|
-
audio_url:
|
|
26
|
-
audio_type:
|
|
33
|
+
audio_url: audioUrl,
|
|
34
|
+
audio_type: audioType,
|
|
27
35
|
duration: decodeXml(duration),
|
|
28
36
|
description: decodeXml(description),
|
|
29
37
|
});
|
|
@@ -94,6 +102,7 @@ export class PodcastService {
|
|
|
94
102
|
if (!resp.ok) throw new Error(`Download HTTP ${resp.status}`);
|
|
95
103
|
const total = parseInt(resp.headers.get('content-length') || '0', 10);
|
|
96
104
|
let downloaded = 0;
|
|
105
|
+
let lastPct = -1;
|
|
97
106
|
const ws = fs.createWriteStream(outputPath + '.part');
|
|
98
107
|
const reader = resp.body.getReader();
|
|
99
108
|
while (true) {
|
|
@@ -102,11 +111,16 @@ export class PodcastService {
|
|
|
102
111
|
ws.write(value);
|
|
103
112
|
downloaded += value.length;
|
|
104
113
|
if (total > 0) {
|
|
105
|
-
|
|
114
|
+
const pct = Math.trunc(downloaded / total * 100);
|
|
115
|
+
if (pct !== lastPct) {
|
|
116
|
+
lastPct = pct;
|
|
117
|
+
process.stdout.write(`\r ⬇️ ${pct}%`);
|
|
118
|
+
}
|
|
106
119
|
}
|
|
107
120
|
}
|
|
108
121
|
ws.end();
|
|
109
|
-
if (total > 0) process.stdout.write('\
|
|
122
|
+
if (total > 0 && lastPct !== 100) process.stdout.write('\r ⬇️ 100%');
|
|
123
|
+
process.stdout.write('\n');
|
|
110
124
|
await new Promise((r) => ws.on('finish', r));
|
|
111
125
|
fs.renameSync(outputPath + '.part', outputPath);
|
|
112
126
|
return true;
|
package/cli/lib/scraper.js
CHANGED
|
@@ -220,7 +220,9 @@ export class SpotifyScraper {
|
|
|
220
220
|
sb += `#EXTINF:-1,${localName}\n`;
|
|
221
221
|
const absPath = path.resolve(matched);
|
|
222
222
|
const absPl = path.resolve(path.dirname(m3uPath));
|
|
223
|
-
|
|
223
|
+
// Echo Nightly compatible: URI-encode the path (keep slashes)
|
|
224
|
+
const rel = encodeURIComponent(relativePath(absPath, absPl)).replace(/%2F/g, '/');
|
|
225
|
+
sb += `${rel}\n`;
|
|
224
226
|
resolved++;
|
|
225
227
|
}
|
|
226
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playlist-admin",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.79",
|
|
4
4
|
"description": "Playlist Administrator CLI - music library pipeline, podcast pipeline and Spotube automation (Node.js)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
"node": ">=20"
|
|
36
36
|
},
|
|
37
37
|
"private": false
|
|
38
|
-
}
|
|
38
|
+
}
|