playlist-admin 2.6.79 → 2.6.81

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.
@@ -32,11 +32,16 @@ export async function convert({ inputPath, outputPath, format = 'mp3', ffmpegPat
32
32
 
33
33
  try {
34
34
  const result = await run(ffmpeg, args, { timeout: 600000 });
35
- if (result.code !== 0) return { ok: false, lufs: null };
35
+ if (result.code !== 0) {
36
+ // Remove the partial/empty output so the file isn't treated as done.
37
+ try { fs.unlinkSync(outputPath); } catch {}
38
+ return { ok: false, lufs: null };
39
+ }
36
40
  const m = result.stderr.match(/Input Integrated:\s+([-\d.]+)/);
37
41
  const lufs = m ? parseFloat(m[1]) : null;
38
42
  return { ok: true, lufs };
39
43
  } catch {
44
+ try { fs.unlinkSync(outputPath); } catch {}
40
45
  return { ok: false, lufs: null };
41
46
  }
42
47
  }
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
- const json = {};
88
- for (const [k, v] of Object.entries(config)) {
89
- json[k] = v;
90
- }
91
- json.spotify_urls = Object.keys(config.urlNames);
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)) out[k] = typeof v === 'number' ? v : parseFloat(v);
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
 
@@ -106,7 +106,8 @@ export class PipelineOrchestrator {
106
106
 
107
107
  const mp3Name = `${stemOf(m4a)}.mp3`;
108
108
  const dest = path.join(config.mp3Path, mp3Name);
109
- if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= fs.statSync(m4a).mtimeMs) {
109
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 0 &&
110
+ fs.statSync(dest).mtimeMs >= fs.statSync(m4a).mtimeMs) {
110
111
  skipped++;
111
112
  scanned++;
112
113
  this._scanLog(scanned, totalM4a, tasks, skipped);
@@ -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, { headers: { 'User-Agent': 'PlaylistAdministrator/2.0' } });
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 || [])
@@ -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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playlist-admin",
3
- "version": "2.6.79",
3
+ "version": "2.6.81",
4
4
  "description": "Playlist Administrator CLI - music library pipeline, podcast pipeline and Spotube automation (Node.js)",
5
5
  "type": "module",
6
6
  "bin": {