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 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);
@@ -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
- sb += `${relativePath(absFilePath, absPlPath)}\n`;
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++;
@@ -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[^>]*url="([^"]*)"[^>]*type="([^"]*)"[^>]*\/?>/);
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: enclosure ? decodeXml(enclosure[1]) : '',
26
- audio_type: enclosure ? enclosure[2] : '',
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
- process.stdout.write(`\r ⬇️ ${(downloaded / total * 100).toFixed(1)}%`);
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('\n');
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;
@@ -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
- sb += `${relativePath(absPath, absPl)}\n`;
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.77",
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
+ }