playlist-admin 2.6.78 → 2.6.80
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/config.js +58 -30
- package/cli/lib/lufs.js +7 -1
- package/cli/lib/pipeline.js +4 -1
- package/cli/lib/playlist_parser.js +6 -2
- package/cli/lib/podcast.js +4 -1
- package/cli/lib/scraper.js +4 -1
- package/package.json +1 -1
package/cli/lib/config.js
CHANGED
|
@@ -37,34 +37,34 @@ export function loadConfig() {
|
|
|
37
37
|
cfg.base_path = cfg.base_path || basePath;
|
|
38
38
|
|
|
39
39
|
const config = {
|
|
40
|
-
basePath: cfg.base_path || '',
|
|
40
|
+
basePath: cfg.base_path || cfg.basePath || '',
|
|
41
41
|
language: cfg.language || 'zh-TW',
|
|
42
|
-
audioFormat: cfg.audio_format || 'mp3',
|
|
43
|
-
maxThreads: cfg.max_threads || 4,
|
|
42
|
+
audioFormat: cfg.audio_format || cfg.audioFormat || 'mp3',
|
|
43
|
+
maxThreads: cfg.max_threads || cfg.maxThreads || 4,
|
|
44
44
|
debugMode: !!cfg.debug_mode,
|
|
45
|
-
enableMetadataEnrichment: !!cfg.enable_metadata_enrichment,
|
|
46
|
-
spotubeExactMatch: cfg.spotube_exact_match !== false,
|
|
47
|
-
spotubeConvertMatchedOnly: !!cfg.spotube_convert_matched_only,
|
|
48
|
-
ffmpegPath: cfg.ffmpeg_path || 'bin/ffmpeg.exe',
|
|
49
|
-
spotubeExePath: cfg.spotube_exe_path || '',
|
|
50
|
-
spotubeDownloadPath: cfg.spotube_download_path || '',
|
|
51
|
-
spotubeFolderName: cfg.spotube_folder_name || 'spotube',
|
|
52
|
-
lyricsFolderName: cfg.lyrics_folder_name || 'Lyrics',
|
|
53
|
-
autoUpdateCheck: cfg.auto_update_check !== false,
|
|
54
|
-
autoDownloadUpdate: !!cfg.auto_download_update,
|
|
55
|
-
enableRetroactiveLyrics: !!cfg.enable_retroactive_lyrics,
|
|
45
|
+
enableMetadataEnrichment: !!(cfg.enable_metadata_enrichment ?? cfg.enableMetadataEnrichment),
|
|
46
|
+
spotubeExactMatch: cfg.spotube_exact_match !== false && cfg.spotubeExactMatch !== false,
|
|
47
|
+
spotubeConvertMatchedOnly: !!(cfg.spotube_convert_matched_only ?? cfg.spotubeConvertMatchedOnly),
|
|
48
|
+
ffmpegPath: cfg.ffmpeg_path || cfg.ffmpegPath || 'bin/ffmpeg.exe',
|
|
49
|
+
spotubeExePath: cfg.spotube_exe_path || cfg.spotubeExePath || '',
|
|
50
|
+
spotubeDownloadPath: cfg.spotube_download_path || cfg.spotubeDownloadPath || '',
|
|
51
|
+
spotubeFolderName: cfg.spotube_folder_name || cfg.spotubeFolderName || 'spotube',
|
|
52
|
+
lyricsFolderName: cfg.lyrics_folder_name || cfg.lyricsFolderName || 'Lyrics',
|
|
53
|
+
autoUpdateCheck: cfg.auto_update_check !== false && cfg.autoUpdateCheck !== false,
|
|
54
|
+
autoDownloadUpdate: !!(cfg.auto_download_update ?? cfg.autoDownloadUpdate),
|
|
55
|
+
enableRetroactiveLyrics: !!(cfg.enable_retroactive_lyrics ?? cfg.enableRetroactiveLyrics),
|
|
56
56
|
theme: cfg.theme || 'dark',
|
|
57
|
-
skippedVersion: cfg.skipped_version || '',
|
|
58
|
-
setupCompleted: !!cfg.setup_completed,
|
|
59
|
-
groqApiKey: cfg.groq_api_key || '',
|
|
60
|
-
groqConcurrency: cfg.groq_concurrency || 3,
|
|
61
|
-
podcastSubscriptions: cfg.podcast_subscriptions || {},
|
|
62
|
-
podcastHistory: cfg.podcast_history || {},
|
|
63
|
-
urlNames: cfg.url_names || {},
|
|
64
|
-
searchNames: cfg.search_names || {},
|
|
65
|
-
spotubeCoords: cfg.spotube_coords || {},
|
|
66
|
-
lastUpdated: cfg.last_updated || {},
|
|
67
|
-
lyricsOffsets: cfg.lyrics_offsets || {},
|
|
57
|
+
skippedVersion: cfg.skipped_version || cfg.skippedVersion || '',
|
|
58
|
+
setupCompleted: !!(cfg.setup_completed ?? cfg.setupCompleted),
|
|
59
|
+
groqApiKey: cfg.groq_api_key || cfg.groqApiKey || '',
|
|
60
|
+
groqConcurrency: cfg.groq_concurrency || cfg.groqConcurrency || 3,
|
|
61
|
+
podcastSubscriptions: cfg.podcast_subscriptions || cfg.podcastSubscriptions || {},
|
|
62
|
+
podcastHistory: cfg.podcast_history || cfg.podcastHistory || {},
|
|
63
|
+
urlNames: cfg.url_names || cfg.urlNames || {},
|
|
64
|
+
searchNames: cfg.search_names || cfg.searchNames || {},
|
|
65
|
+
spotubeCoords: cfg.spotube_coords || cfg.spotubeCoords || {},
|
|
66
|
+
lastUpdated: cfg.last_updated || cfg.lastUpdated || {},
|
|
67
|
+
lyricsOffsets: cfg.lyrics_offsets || cfg.lyricsOffsets || {},
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
if (config.basePath) derivePaths(config);
|
|
@@ -84,11 +84,39 @@ export function saveConfig(config) {
|
|
|
84
84
|
}
|
|
85
85
|
try {
|
|
86
86
|
fs.writeFileSync(lock, '');
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// Write in the Python/Dart-compatible snake_case schema. Writing JS
|
|
88
|
+
// camelCase here would break the GUI/Flutter app (and this reader).
|
|
89
|
+
const json = {
|
|
90
|
+
base_path: config.basePath,
|
|
91
|
+
language: config.language,
|
|
92
|
+
audio_format: config.audioFormat,
|
|
93
|
+
max_threads: config.maxThreads,
|
|
94
|
+
debug_mode: config.debugMode,
|
|
95
|
+
enable_metadata_enrichment: config.enableMetadataEnrichment,
|
|
96
|
+
spotube_exact_match: config.spotubeExactMatch,
|
|
97
|
+
spotube_convert_matched_only: config.spotubeConvertMatchedOnly,
|
|
98
|
+
ffmpeg_path: config.ffmpegPath,
|
|
99
|
+
spotube_exe_path: config.spotubeExePath,
|
|
100
|
+
spotube_download_path: config.spotubeDownloadPath,
|
|
101
|
+
spotube_folder_name: config.spotubeFolderName,
|
|
102
|
+
lyrics_folder_name: config.lyricsFolderName,
|
|
103
|
+
auto_update_check: config.autoUpdateCheck,
|
|
104
|
+
auto_download_update: config.autoDownloadUpdate,
|
|
105
|
+
enable_retroactive_lyrics: config.enableRetroactiveLyrics,
|
|
106
|
+
theme: config.theme,
|
|
107
|
+
skipped_version: config.skippedVersion,
|
|
108
|
+
setup_completed: config.setupCompleted,
|
|
109
|
+
groq_api_key: config.groqApiKey,
|
|
110
|
+
groq_concurrency: config.groqConcurrency,
|
|
111
|
+
podcast_subscriptions: config.podcastSubscriptions,
|
|
112
|
+
podcast_history: config.podcastHistory,
|
|
113
|
+
url_names: config.urlNames,
|
|
114
|
+
search_names: config.searchNames,
|
|
115
|
+
spotube_coords: config.spotubeCoords,
|
|
116
|
+
last_updated: config.lastUpdated,
|
|
117
|
+
lyrics_offsets: config.lyricsOffsets,
|
|
118
|
+
spotify_urls: Object.keys(config.urlNames),
|
|
119
|
+
};
|
|
92
120
|
fs.writeFileSync(path_, JSON.stringify(json, null, 2), 'utf-8');
|
|
93
121
|
} finally {
|
|
94
122
|
try { fs.unlinkSync(lock); } catch {}
|
package/cli/lib/lufs.js
CHANGED
|
@@ -15,7 +15,11 @@ export class LufsService {
|
|
|
15
15
|
_load(fmt) {
|
|
16
16
|
const data = readJson(this._cacheFile(fmt), {});
|
|
17
17
|
const out = {};
|
|
18
|
-
for (const [k, v] of Object.entries(data))
|
|
18
|
+
for (const [k, v] of Object.entries(data)) {
|
|
19
|
+
// GUI (Python/Dart) writes unnormalized keys like "...\Playlists\..\mp3\x.mp3".
|
|
20
|
+
// Normalize so lookups (already normalized) hit the existing cache.
|
|
21
|
+
out[path.normalize(k)] = typeof v === 'number' ? v : parseFloat(v);
|
|
22
|
+
}
|
|
19
23
|
return out;
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -24,6 +28,8 @@ export class LufsService {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
_resolvePlaylistPath(entry, playlistsPath, basePath, libraryPath) {
|
|
31
|
+
let decodePath = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
32
|
+
entry = decodePath(entry);
|
|
27
33
|
if (!entry) return null;
|
|
28
34
|
if (!entry.includes('.') && !entry.includes('\\') && !entry.includes('/')) return null;
|
|
29
35
|
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
|
@@ -206,7 +206,10 @@ export class PodcastService {
|
|
|
206
206
|
|
|
207
207
|
async searchPodcasts(query) {
|
|
208
208
|
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(query)}&media=podcast&limit=20`;
|
|
209
|
-
const resp = await fetch(url, {
|
|
209
|
+
const resp = await fetch(url, {
|
|
210
|
+
headers: { 'User-Agent': 'PlaylistAdministrator/2.0' },
|
|
211
|
+
signal: AbortSignal.timeout(30000),
|
|
212
|
+
});
|
|
210
213
|
if (resp.status !== 200) throw new Error(`搜尋失敗 (${resp.status})`);
|
|
211
214
|
const data = await resp.json();
|
|
212
215
|
return (data.results || [])
|
package/cli/lib/scraper.js
CHANGED
|
@@ -119,6 +119,7 @@ export class SpotifyScraper {
|
|
|
119
119
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
120
120
|
'Accept-Language': 'zh-TW,zh;q=0.9,en;q=0.5',
|
|
121
121
|
},
|
|
122
|
+
signal: AbortSignal.timeout(60000),
|
|
122
123
|
});
|
|
123
124
|
if (resp.status !== 200) {
|
|
124
125
|
this.log(` HTTP ${resp.status}`);
|
|
@@ -220,7 +221,9 @@ export class SpotifyScraper {
|
|
|
220
221
|
sb += `#EXTINF:-1,${localName}\n`;
|
|
221
222
|
const absPath = path.resolve(matched);
|
|
222
223
|
const absPl = path.resolve(path.dirname(m3uPath));
|
|
223
|
-
|
|
224
|
+
// Echo Nightly compatible: URI-encode the path (keep slashes)
|
|
225
|
+
const rel = encodeURIComponent(relativePath(absPath, absPl)).replace(/%2F/g, '/');
|
|
226
|
+
sb += `${rel}\n`;
|
|
224
227
|
resolved++;
|
|
225
228
|
}
|
|
226
229
|
}
|