goblin-malin 0.1.13 → 0.1.14

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.
@@ -10,7 +10,7 @@ import {
10
10
  getCacheDir,
11
11
  globalLogger,
12
12
  inkTransport
13
- } from "./chunk-4NSWYXSP.js";
13
+ } from "./chunk-QSUGRYOC.js";
14
14
 
15
15
  // src/index.tsx
16
16
  import React70 from "react";
@@ -1246,6 +1246,48 @@ var Env = class {
1246
1246
  }
1247
1247
  }
1248
1248
  async getVariablesWithWizard(config) {
1249
+ if (config.modes && config.modeEnvVar) {
1250
+ const modeEnvVar = config.modeEnvVar;
1251
+ const chosenModeId = process.env[modeEnvVar] ?? config.modes[0].id;
1252
+ const activeMode = config.modes.find((m) => m.id === chosenModeId) ?? config.modes[0];
1253
+ const missingModeFields = activeMode.fields.filter((f) => !process.env[f.envVar]);
1254
+ if (missingModeFields.length === 0) {
1255
+ return {
1256
+ [modeEnvVar]: activeMode.id,
1257
+ ...Object.fromEntries(activeMode.fields.map((f) => [
1258
+ f.envVar,
1259
+ process.env[f.envVar]
1260
+ ]))
1261
+ };
1262
+ }
1263
+ let values;
1264
+ try {
1265
+ values = await this.task.getPrompt().askSetupWizard(config);
1266
+ } catch (error) {
1267
+ this.persistProviderDisabled(config);
1268
+ throw error;
1269
+ }
1270
+ const nonEmpty = Object.fromEntries(Object.entries(values).filter(([, v]) => v.trim()));
1271
+ try {
1272
+ if (config.envSection && Object.keys(nonEmpty).length > 0) {
1273
+ await saveEnvVarsGroup(nonEmpty, config.envSection);
1274
+ } else {
1275
+ for (const [key, value] of Object.entries(nonEmpty)) {
1276
+ await saveEnvVar(key, value);
1277
+ }
1278
+ }
1279
+ this.logger.info(`Saved wizard vars to .env file.`);
1280
+ } catch (error) {
1281
+ this.logger.error(`Failed to save wizard vars to .env file:`, {
1282
+ error
1283
+ });
1284
+ throw new Error(`Could not persist environment variables to .env file`);
1285
+ }
1286
+ for (const [key, value] of Object.entries(values)) {
1287
+ process.env[key] = value;
1288
+ }
1289
+ return values;
1290
+ }
1249
1291
  const missing = config.fields.filter((f) => !process.env[f.envVar]);
1250
1292
  if (missing.length > 0) {
1251
1293
  let values;
@@ -1561,6 +1603,13 @@ var BUILTIN_PROVIDERS = {
1561
1603
  color: "#f76c1b",
1562
1604
  colorSubtle: "#7a3000",
1563
1605
  colorBright: "#ff8c3a"
1606
+ },
1607
+ spotifyUrlInfo: {
1608
+ label: "Scraped",
1609
+ acronym: "SCRAPED SPOTIFY",
1610
+ color: "#3bb0a0",
1611
+ colorSubtle: "#0f5a52",
1612
+ colorBright: "#56d4c4"
1564
1613
  }
1565
1614
  };
1566
1615
  var FALLBACK = {
@@ -2273,6 +2322,144 @@ var MetadataService = class extends ServiceBase {
2273
2322
  }
2274
2323
  };
2275
2324
 
2325
+ // src/flows/musicDownloadFlow/services/apis/spotify-url-info-client.ts
2326
+ import { createRequire } from "module";
2327
+ var require2 = createRequire(import.meta.url);
2328
+ var createSpotifyUrlInfo = require2("spotify-url-info");
2329
+ var api = createSpotifyUrlInfo(fetch);
2330
+ function getSpotifyEmbedDetails(url) {
2331
+ return api.getDetails(url);
2332
+ }
2333
+ __name(getSpotifyEmbedDetails, "getSpotifyEmbedDetails");
2334
+
2335
+ // src/flows/musicDownloadFlow/services/metadata-providers/spotify/convertSpotifyUrlInfoToTrack.ts
2336
+ function parseArtistNames(artistName) {
2337
+ if (!artistName) return [];
2338
+ return artistName.split(/,|&|\bfeat\.?\b|\bft\.?\b/i).map((n) => n.trim()).filter((n) => n.length > 0).map((name) => ({
2339
+ type: "artist",
2340
+ name
2341
+ }));
2342
+ }
2343
+ __name(parseArtistNames, "parseArtistNames");
2344
+ function extractTrackId(url) {
2345
+ try {
2346
+ const parsed = new URL(url);
2347
+ const trackMatch = parsed.pathname.match(/\/(?:intl-[a-z]{2}\/)?track\/([a-zA-Z0-9]+)/);
2348
+ return trackMatch ? trackMatch[1] : null;
2349
+ } catch {
2350
+ return null;
2351
+ }
2352
+ }
2353
+ __name(extractTrackId, "extractTrackId");
2354
+ function convertSpotifyUrlInfoToTrack(url, details, options = {}) {
2355
+ const track = details.tracks[0];
2356
+ const id = extractTrackId(url) ?? track.uri?.split(":").pop() ?? "";
2357
+ return {
2358
+ id,
2359
+ trackName: track.name,
2360
+ artists: parseArtistNames(track.artist),
2361
+ duration: track.duration,
2362
+ url,
2363
+ uri: `SPOTIFY::TRACK::${id}`,
2364
+ nativeAppUriDesktop: `spotify:track:${id}`,
2365
+ platform: "spotify",
2366
+ apiProvider: options.isFallback ? "spotify" : "spotifyUrlInfo",
2367
+ fetchedBy: "spotifyUrlInfo",
2368
+ fetchedAt: /* @__PURE__ */ new Date(),
2369
+ type: "track"
2370
+ };
2371
+ }
2372
+ __name(convertSpotifyUrlInfoToTrack, "convertSpotifyUrlInfoToTrack");
2373
+
2374
+ // src/flows/musicDownloadFlow/saveSettings.ts
2375
+ import fs3 from "fs";
2376
+
2377
+ // src/flows/musicDownloadFlow/settings.ts
2378
+ import * as os from "os";
2379
+ import * as path2 from "path";
2380
+ function extractProviderDefaults(registry) {
2381
+ const result = {};
2382
+ for (const key of registry.getFactories().keys()) {
2383
+ const schema = registry.getConstructor(key)?.defaultSettings;
2384
+ result[key] = schema ? Object.fromEntries(Object.entries(schema).map(([k, v]) => [
2385
+ k,
2386
+ v.defaultValue
2387
+ ])) : {
2388
+ enabled: true
2389
+ };
2390
+ }
2391
+ return result;
2392
+ }
2393
+ __name(extractProviderDefaults, "extractProviderDefaults");
2394
+ function getPlatformDefaultMusicOutputDir() {
2395
+ switch (process.platform) {
2396
+ case "win32":
2397
+ return path2.join(os.homedir(), "Music", "GoblinMalin");
2398
+ case "darwin":
2399
+ return path2.join(os.homedir(), "Music", "GoblinMalin");
2400
+ default:
2401
+ return path2.join(process.env.XDG_MUSIC_DIR ?? path2.join(os.homedir(), "Music"), "GoblinMalin");
2402
+ }
2403
+ }
2404
+ __name(getPlatformDefaultMusicOutputDir, "getPlatformDefaultMusicOutputDir");
2405
+ var MEDIA_OUTPUT_DIR = getPlatformDefaultMusicOutputDir();
2406
+ var BASE_DEFAULT_MUSIC_DOWNLOAD_FLOW_SETTINGS = {
2407
+ metadata: {
2408
+ autoChooseBestSource: false,
2409
+ providers: {},
2410
+ discoveryProviders: {}
2411
+ },
2412
+ download: {
2413
+ autoChooseBestSource: false,
2414
+ autoSaveToOutputDir: true,
2415
+ autoDeleteTempAfter24h: false,
2416
+ autoRelocateMissingFiles: false,
2417
+ outputDir: MEDIA_OUTPUT_DIR,
2418
+ outputTemporaryDir: path2.join(MEDIA_OUTPUT_DIR, "tmp"),
2419
+ providers: {}
2420
+ },
2421
+ ui: {
2422
+ columnRatios: {}
2423
+ }
2424
+ };
2425
+
2426
+ // src/flows/musicDownloadFlow/saveSettings.ts
2427
+ function getFlowSettings() {
2428
+ return SettingsStore.getInstance().getFlowSettings("music-downloader", BASE_DEFAULT_MUSIC_DOWNLOAD_FLOW_SETTINGS);
2429
+ }
2430
+ __name(getFlowSettings, "getFlowSettings");
2431
+ function getTempDownloadDir() {
2432
+ return getFlowSettings().download.outputTemporaryDir;
2433
+ }
2434
+ __name(getTempDownloadDir, "getTempDownloadDir");
2435
+ function getDownloadProviderSettings(providerKey) {
2436
+ return getFlowSettings().download.providers[providerKey] ?? {};
2437
+ }
2438
+ __name(getDownloadProviderSettings, "getDownloadProviderSettings");
2439
+ function getMetadataProviderSettings(providerKey) {
2440
+ return getFlowSettings().metadata.providers[providerKey] ?? {};
2441
+ }
2442
+ __name(getMetadataProviderSettings, "getMetadataProviderSettings");
2443
+ function clearTempDownloads() {
2444
+ const dir = getTempDownloadDir();
2445
+ if (fs3.existsSync(dir)) {
2446
+ fs3.rmSync(dir, {
2447
+ recursive: true,
2448
+ force: true
2449
+ });
2450
+ }
2451
+ globalLogger.info("Temporary downloads cleared");
2452
+ }
2453
+ __name(clearTempDownloads, "clearTempDownloads");
2454
+ function getSaveSettings() {
2455
+ const s = getFlowSettings();
2456
+ return {
2457
+ outputDir: s.download.outputDir,
2458
+ includeMusicBrainzTags: false
2459
+ };
2460
+ }
2461
+ __name(getSaveSettings, "getSaveSettings");
2462
+
2276
2463
  // src/flows/musicDownloadFlow/services/metadata-providers/spotify/SpotifyService.ts
2277
2464
  function _ts_decorate(decorators, target, key, desc) {
2278
2465
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -2301,6 +2488,11 @@ var SpotifyService = class _SpotifyService extends MetadataService {
2301
2488
  label: "Enable",
2302
2489
  defaultValue: true,
2303
2490
  kind: "checkbox"
2491
+ },
2492
+ fallbackToWeb: {
2493
+ label: "Fallback on Spotify Web if API is unavailable",
2494
+ defaultValue: true,
2495
+ kind: "checkbox"
2304
2496
  }
2305
2497
  };
2306
2498
  static setupWizard = {
@@ -2311,61 +2503,87 @@ var SpotifyService = class _SpotifyService extends MetadataService {
2311
2503
  name: "SPOTIFY",
2312
2504
  url: "https://developer.spotify.com/dashboard"
2313
2505
  },
2314
- description: [
2315
- {
2316
- type: "note",
2317
- text: "You need a Premium Spotify Account to access the Spotify API."
2318
- },
2319
- {
2320
- type: "paragraph",
2321
- text: "Steps to setup Spotify API access:"
2322
- },
2323
- {
2324
- type: "orderedList",
2325
- items: [
2506
+ description: [],
2507
+ fields: [],
2508
+ modeEnvVar: "SPOTIFY_AUTH_MODE",
2509
+ modes: [
2510
+ {
2511
+ id: "official",
2512
+ label: "Official Spotify API (needs Premium account)",
2513
+ description: "Best metadata (album, ISRC, track number).",
2514
+ details: [
2326
2515
  {
2327
- type: "link",
2328
- text: "Log in to the Spotify Developer Dashboard",
2329
- url: "https://developer.spotify.com/dashboard"
2516
+ type: "note",
2517
+ text: "You need a Premium Spotify Account to access the Spotify API."
2330
2518
  },
2331
2519
  {
2332
- type: "text",
2333
- text: 'Click "Create app"'
2520
+ type: "paragraph",
2521
+ text: "Steps to setup Spotify API access:"
2334
2522
  },
2335
2523
  {
2336
- type: "text",
2337
- text: "Copy the CLIENT_ID and CLIENT_SECRET from the app page"
2524
+ type: "orderedList",
2525
+ items: [
2526
+ {
2527
+ type: "link",
2528
+ text: "Log in to the Spotify Developer Dashboard",
2529
+ url: "https://developer.spotify.com/dashboard"
2530
+ },
2531
+ {
2532
+ type: "text",
2533
+ text: 'Click "Create app"'
2534
+ },
2535
+ {
2536
+ type: "text",
2537
+ text: "Copy the CLIENT_ID and CLIENT_SECRET from the app page"
2538
+ }
2539
+ ]
2540
+ }
2541
+ ],
2542
+ fields: [
2543
+ {
2544
+ envVar: "SPOTIFY_CLIENT_ID",
2545
+ label: "CLIENT_ID",
2546
+ hint: "e.g. b94c59cdcd\u2026"
2547
+ },
2548
+ {
2549
+ envVar: "SPOTIFY_CLIENT_SECRET",
2550
+ label: "CLIENT_SECRET",
2551
+ hint: "e.g. fa5a8a70ab\u2026"
2338
2552
  }
2339
2553
  ]
2340
- }
2341
- ],
2342
- fields: [
2343
- {
2344
- envVar: "SPOTIFY_CLIENT_ID",
2345
- label: "CLIENT_ID",
2346
- hint: "e.g. b94c59cdcd\u2026"
2347
2554
  },
2348
2555
  {
2349
- envVar: "SPOTIFY_CLIENT_SECRET",
2350
- label: "CLIENT_SECRET",
2351
- hint: "e.g. fa5a8a70ab\u2026"
2556
+ id: "scrape",
2557
+ label: "Scrape Spotify Web page (reads public page)",
2558
+ description: "Limited metadata (no album, no ISRC).\nMay break if Spotify changes embed page.",
2559
+ fields: []
2352
2560
  }
2353
2561
  ]
2354
2562
  };
2355
2563
  static cellComponent = SpotifyCell;
2356
- static client;
2564
+ static client = null;
2565
+ static authMode = null;
2357
2566
  constructor(task, logger) {
2358
2567
  super("SpotifyService", task, logger);
2359
2568
  }
2360
- async getClient() {
2569
+ async resolveAuth() {
2361
2570
  return this.runExclusive("init", async () => {
2362
- if (!_SpotifyService.client) {
2363
- const vars = await this.env.getVariablesWithWizard(_SpotifyService.setupWizard);
2364
- _SpotifyService.client = SpotifyApi.withClientCredentials(vars["SPOTIFY_CLIENT_ID"], vars["SPOTIFY_CLIENT_SECRET"]);
2571
+ if (_SpotifyService.authMode !== null) return;
2572
+ await this.env.getVariablesWithWizard(_SpotifyService.setupWizard);
2573
+ const mode = process.env.SPOTIFY_AUTH_MODE ?? "official";
2574
+ _SpotifyService.authMode = mode;
2575
+ if (mode === "official") {
2576
+ _SpotifyService.client = SpotifyApi.withClientCredentials(process.env.SPOTIFY_CLIENT_ID, process.env.SPOTIFY_CLIENT_SECRET);
2365
2577
  }
2366
- return _SpotifyService.client;
2367
2578
  });
2368
2579
  }
2580
+ async getClient() {
2581
+ await this.resolveAuth();
2582
+ if (!_SpotifyService.client) {
2583
+ throw new Error("Spotify client not initialized (scrape mode active)");
2584
+ }
2585
+ return _SpotifyService.client;
2586
+ }
2369
2587
  /**
2370
2588
  * Fetches track details from the Spotify API.
2371
2589
  *
@@ -2461,27 +2679,76 @@ var SpotifyService = class _SpotifyService extends MetadataService {
2461
2679
  };
2462
2680
  return null;
2463
2681
  }
2682
+ async fetchViaUrlInfo(url, trackId, isFallback) {
2683
+ this.logger.info(`Fetching Spotify track via spotify-url-info: "${trackId}"\u2026`);
2684
+ this.status.set({
2685
+ type: StatusType.Processing,
2686
+ message: "Get spotify track info (embed)",
2687
+ timeTracking: true,
2688
+ progress: 0
2689
+ });
2690
+ try {
2691
+ const details = await getSpotifyEmbedDetails(url);
2692
+ this.status.clear();
2693
+ return convertSpotifyUrlInfoToTrack(url, details, {
2694
+ isFallback
2695
+ });
2696
+ } catch (error) {
2697
+ this.logger.error(`Error fetching Spotify track via spotify-url-info for ID ${trackId}:`, {
2698
+ error
2699
+ });
2700
+ this.status.set({
2701
+ type: StatusType.Error,
2702
+ message: "Error fetching Spotify track info (embed)"
2703
+ });
2704
+ throw error;
2705
+ }
2706
+ }
2464
2707
  async getTrackMetadata(url) {
2708
+ await this.resolveAuth();
2465
2709
  const trackId = _SpotifyService.parseUrl(url)?.id;
2466
2710
  if (!trackId) {
2467
2711
  throw new Error(`Invalid Spotify track URL: ${url}`);
2468
2712
  }
2469
- const spotifyTrack = await this.getTrackInfo(trackId);
2470
- if (!spotifyTrack) {
2471
- throw new Error(`Could not fetch Spotify track: ${trackId}`);
2713
+ const mode = _SpotifyService.authMode ?? "official";
2714
+ if (mode === "scrape") {
2715
+ return this.fetchViaUrlInfo(url, trackId, false);
2716
+ }
2717
+ try {
2718
+ const spotifyTrack = await this.getTrackInfo(trackId);
2719
+ if (!spotifyTrack) {
2720
+ throw new Error(`Could not fetch Spotify track: ${trackId}`);
2721
+ }
2722
+ const standardTrack = this.convertSpotifyTrack(spotifyTrack, url);
2723
+ const metadata = {
2724
+ ...standardTrack,
2725
+ platform: "spotify",
2726
+ apiProvider: "spotify",
2727
+ uri: `SPOTIFY::TRACK::${spotifyTrack.id}`,
2728
+ fetchedAt: /* @__PURE__ */ new Date(),
2729
+ type: "track"
2730
+ };
2731
+ return metadata;
2732
+ } catch (error) {
2733
+ const fallbackEnabled = getMetadataProviderSettings("spotify").fallbackToWeb !== false;
2734
+ if (!fallbackEnabled) {
2735
+ this.logger.error(`Official Spotify API failed for "${trackId}" and Spotify Web fallback is disabled`, {
2736
+ error
2737
+ });
2738
+ throw error;
2739
+ }
2740
+ this.logger.warn(`Official Spotify API failed for "${trackId}", falling back to spotify-url-info`, {
2741
+ error
2742
+ });
2743
+ return this.fetchViaUrlInfo(url, trackId, true);
2472
2744
  }
2473
- const standardTrack = this.convertSpotifyTrack(spotifyTrack, url);
2474
- const metadata = {
2475
- ...standardTrack,
2476
- platform: "spotify",
2477
- apiProvider: "spotify",
2478
- uri: `SPOTIFY::TRACK::${spotifyTrack.id}`,
2479
- fetchedAt: /* @__PURE__ */ new Date(),
2480
- type: "track"
2481
- };
2482
- return metadata;
2483
2745
  }
2484
2746
  async searchTrack(sourceTrackMetadata) {
2747
+ await this.resolveAuth();
2748
+ const mode = _SpotifyService.authMode ?? "official";
2749
+ if (mode === "scrape") {
2750
+ throw new Error("Spotify search is not available in scrape mode. Switch to official API mode for search support.");
2751
+ }
2485
2752
  const client = await this.getClient();
2486
2753
  const artist = sourceTrackMetadata.artists?.[0]?.name;
2487
2754
  const trackName = sourceTrackMetadata.trackName;
@@ -2952,15 +3219,15 @@ var DiscoveryMetadataService = class extends ServiceBase {
2952
3219
  };
2953
3220
 
2954
3221
  // src/flows/musicDownloadFlow/services/apis/songlink-client.ts
2955
- import path2 from "path";
3222
+ import path3 from "path";
2956
3223
 
2957
3224
  // src/flows/musicDownloadFlow/services/metadata-providers/songlink/json.ts
2958
- import fs3 from "fs/promises";
3225
+ import fs4 from "fs/promises";
2959
3226
  async function ensureCacheDir() {
2960
3227
  try {
2961
- await fs3.access(getCacheDir());
3228
+ await fs4.access(getCacheDir());
2962
3229
  } catch {
2963
- await fs3.mkdir(getCacheDir(), {
3230
+ await fs4.mkdir(getCacheDir(), {
2964
3231
  recursive: true
2965
3232
  });
2966
3233
  }
@@ -2969,7 +3236,7 @@ __name(ensureCacheDir, "ensureCacheDir");
2969
3236
  async function loadJsonFile(filePath, defaultValue) {
2970
3237
  await ensureCacheDir();
2971
3238
  try {
2972
- const content = await fs3.readFile(filePath, "utf-8");
3239
+ const content = await fs4.readFile(filePath, "utf-8");
2973
3240
  return JSON.parse(content);
2974
3241
  } catch {
2975
3242
  return defaultValue;
@@ -2977,7 +3244,7 @@ async function loadJsonFile(filePath, defaultValue) {
2977
3244
  }
2978
3245
  __name(loadJsonFile, "loadJsonFile");
2979
3246
  async function saveJsonFile(filePath, data) {
2980
- await fs3.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
3247
+ await fs4.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
2981
3248
  }
2982
3249
  __name(saveJsonFile, "saveJsonFile");
2983
3250
 
@@ -2989,7 +3256,7 @@ __name(sleep, "sleep");
2989
3256
 
2990
3257
  // src/flows/musicDownloadFlow/services/apis/songlink-client.ts
2991
3258
  var SONGLINK_API_BASE_URL = "https://api.song.link/v1-alpha.1/links";
2992
- var SONGLINK_RATE_PATH = path2.join(getCacheDir(), "songlink_rate.json");
3259
+ var SONGLINK_RATE_PATH = path3.join(getCacheDir(), "songlink_rate.json");
2993
3260
  var SONGLINK_HOURLY_LIMIT = 19;
2994
3261
  var HOUR_MS = 36e5;
2995
3262
  var SONGLINK_MAX_ATTEMPTS = 6;
@@ -3124,14 +3391,14 @@ var SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set([
3124
3391
  "appleMusic",
3125
3392
  "itunes"
3126
3393
  ]);
3127
- function parseArtistNames(artistName) {
3394
+ function parseArtistNames2(artistName) {
3128
3395
  if (!artistName) return [];
3129
3396
  return artistName.split(/,|&|\bfeat\.?\b|\bft\.?\b/i).map((n) => n.trim()).filter((n) => n.length > 0);
3130
3397
  }
3131
- __name(parseArtistNames, "parseArtistNames");
3398
+ __name(parseArtistNames2, "parseArtistNames");
3132
3399
  function convertSonglinkEntityToTrackMetadata(platformKey, platformLink, entity) {
3133
3400
  if (!SUPPORTED_PLATFORMS.has(platformKey)) return null;
3134
- const artistNames = parseArtistNames(entity.artistName);
3401
+ const artistNames = parseArtistNames2(entity.artistName);
3135
3402
  const platform = platformKey;
3136
3403
  const apiProvider = entity.apiProvider;
3137
3404
  const uriPrefix = platformKey === "youtubeMusic" ? "youtube" : platformKey;
@@ -3280,10 +3547,10 @@ var MusicBrainzClient = class _MusicBrainzClient {
3280
3547
  * Prefers the ISRC (an exact, cross-platform identifier) and falls back to a title + artist search.
3281
3548
  */
3282
3549
  async findRecordingId(params) {
3283
- const api = this.getApi();
3550
+ const api2 = this.getApi();
3284
3551
  const { isrc, trackName, artistName } = params;
3285
3552
  if (isrc) {
3286
- const byIsrc = await api.search("recording", {
3553
+ const byIsrc = await api2.search("recording", {
3287
3554
  query: {
3288
3555
  isrc
3289
3556
  }
@@ -3296,7 +3563,7 @@ var MusicBrainzClient = class _MusicBrainzClient {
3296
3563
  recording: trackName
3297
3564
  };
3298
3565
  if (artistName) query.artist = artistName;
3299
- const byName = await api.search("recording", {
3566
+ const byName = await api2.search("recording", {
3300
3567
  query
3301
3568
  });
3302
3569
  const top = byName.recordings?.[0];
@@ -3549,9 +3816,9 @@ var DownloadService = class extends ServiceBase {
3549
3816
  };
3550
3817
 
3551
3818
  // src/utils/ffmpeg-setup.ts
3552
- import * as fs4 from "fs/promises";
3819
+ import * as fs5 from "fs/promises";
3553
3820
  import { createWriteStream } from "fs";
3554
- import * as path3 from "path";
3821
+ import * as path4 from "path";
3555
3822
  import * as https from "https";
3556
3823
  import AdmZip from "adm-zip";
3557
3824
  async function ensureFfmpeg(autoDownloadBinary = true) {
@@ -3576,21 +3843,21 @@ async function ensureFfmpeg(autoDownloadBinary = true) {
3576
3843
  }
3577
3844
  const versionDate = new Date(release.published_at).toISOString().split("T")[0];
3578
3845
  const binaryName = `ffmpeg_${versionDate}.exe`;
3579
- const binaryPath = path3.join(getBinDir(), binaryName);
3846
+ const binaryPath = path4.join(getBinDir(), binaryName);
3580
3847
  try {
3581
- await fs4.access(binaryPath);
3848
+ await fs5.access(binaryPath);
3582
3849
  globalLogger.info(`ffmpeg ${versionDate} already installed at ${binaryPath}`);
3583
3850
  return binaryPath;
3584
3851
  } catch {
3585
3852
  globalLogger.info(`ffmpeg ${versionDate} not found, downloading\u2026`);
3586
3853
  }
3587
- await fs4.mkdir(getBinDir(), {
3854
+ await fs5.mkdir(getBinDir(), {
3588
3855
  recursive: true
3589
3856
  });
3590
3857
  await cleanupOldVersions("ffmpeg_", binaryName);
3591
3858
  const zipName = "ffmpeg-master-latest-win64-gpl.zip";
3592
3859
  const downloadUrl = `https://github.com/BtbN/FFmpeg-Builds/releases/download/${release.tag_name}/${zipName}`;
3593
- const zipPath = path3.join(getBinDir(), zipName);
3860
+ const zipPath = path4.join(getBinDir(), zipName);
3594
3861
  await downloadFile(downloadUrl, zipPath);
3595
3862
  globalLogger.debug("Extracting ffmpeg.exe\u2026");
3596
3863
  const zip = new AdmZip(zipPath);
@@ -3600,9 +3867,9 @@ async function ensureFfmpeg(autoDownloadBinary = true) {
3600
3867
  throw new Error("ffmpeg.exe not found in the downloaded archive");
3601
3868
  }
3602
3869
  zip.extractEntryTo(ffmpegEntry, getBinDir(), false, true);
3603
- const extractedPath = path3.join(getBinDir(), "ffmpeg.exe");
3604
- await fs4.rename(extractedPath, binaryPath);
3605
- await fs4.unlink(zipPath);
3870
+ const extractedPath = path4.join(getBinDir(), "ffmpeg.exe");
3871
+ await fs5.rename(extractedPath, binaryPath);
3872
+ await fs5.unlink(zipPath);
3606
3873
  globalLogger.info(`Successfully downloaded ffmpeg ${versionDate} to ${binaryPath}`);
3607
3874
  return binaryPath;
3608
3875
  } catch {
@@ -3617,14 +3884,14 @@ async function ensureFfmpeg(autoDownloadBinary = true) {
3617
3884
  __name(ensureFfmpeg, "ensureFfmpeg");
3618
3885
  async function findExistingBinary(prefix, suffix) {
3619
3886
  try {
3620
- const files = await fs4.readdir(getBinDir());
3887
+ const files = await fs5.readdir(getBinDir());
3621
3888
  const binaries = files.filter((file) => file.startsWith(prefix) && file.endsWith(suffix));
3622
3889
  if (binaries.length === 0) {
3623
3890
  return null;
3624
3891
  }
3625
3892
  binaries.sort().reverse();
3626
- const binaryPath = path3.join(getBinDir(), binaries[0]);
3627
- await fs4.access(binaryPath);
3893
+ const binaryPath = path4.join(getBinDir(), binaries[0]);
3894
+ await fs5.access(binaryPath);
3628
3895
  return binaryPath;
3629
3896
  } catch (error) {
3630
3897
  globalLogger.warn(`Failed to find existing binary: ${error}`);
@@ -3659,10 +3926,10 @@ async function getLatestFfmpegRelease() {
3659
3926
  __name(getLatestFfmpegRelease, "getLatestFfmpegRelease");
3660
3927
  async function cleanupOldVersions(prefix, currentVersion) {
3661
3928
  try {
3662
- const files = await fs4.readdir(getBinDir());
3929
+ const files = await fs5.readdir(getBinDir());
3663
3930
  const oldVersions = files.filter((file) => file.startsWith(prefix) && file.endsWith(".exe") && file !== currentVersion);
3664
3931
  for (const file of oldVersions) {
3665
- await fs4.unlink(path3.join(getBinDir(), file));
3932
+ await fs5.unlink(path4.join(getBinDir(), file));
3666
3933
  globalLogger.info(`Cleaned up old version: ${file}`);
3667
3934
  }
3668
3935
  } catch (error) {
@@ -3690,7 +3957,7 @@ async function downloadFile(url, destination) {
3690
3957
  resolve();
3691
3958
  });
3692
3959
  fileStream.on("error", (error) => {
3693
- fs4.unlink(destination).catch(() => {
3960
+ fs5.unlink(destination).catch(() => {
3694
3961
  });
3695
3962
  reject(error);
3696
3963
  });
@@ -3719,9 +3986,9 @@ async function readFileInfo(filePath, fallbackDurationMs) {
3719
3986
  __name(readFileInfo, "readFileInfo");
3720
3987
 
3721
3988
  // src/flows/musicDownloadFlow/services/download-providers/ytdlp/ytdlp-setup.ts
3722
- import * as fs5 from "fs/promises";
3989
+ import * as fs6 from "fs/promises";
3723
3990
  import { createWriteStream as createWriteStream2 } from "fs";
3724
- import * as path4 from "path";
3991
+ import * as path5 from "path";
3725
3992
  import * as https2 from "https";
3726
3993
  async function ensureYtDlpSetup(autoDownloadBinary = true) {
3727
3994
  if (!autoDownloadBinary) {
@@ -3744,15 +4011,15 @@ async function ensureYtDlpSetup(autoDownloadBinary = true) {
3744
4011
  throw new Error("yt-dlp release not found");
3745
4012
  }
3746
4013
  const binaryName = `yt-dlp_${latestVersion}.exe`;
3747
- const binaryPath = path4.join(getBinDir(), binaryName);
4014
+ const binaryPath = path5.join(getBinDir(), binaryName);
3748
4015
  try {
3749
- await fs5.access(binaryPath);
4016
+ await fs6.access(binaryPath);
3750
4017
  globalLogger.info(`yt-dlp ${latestVersion} already installed at ${binaryPath}`);
3751
4018
  return binaryPath;
3752
4019
  } catch {
3753
4020
  globalLogger.info(`yt-dlp ${latestVersion} not found, downloading\u2026`);
3754
4021
  }
3755
- await fs5.mkdir(getBinDir(), {
4022
+ await fs6.mkdir(getBinDir(), {
3756
4023
  recursive: true
3757
4024
  });
3758
4025
  await cleanupOldVersions2("yt-dlp_", binaryName);
@@ -3772,14 +4039,14 @@ async function ensureYtDlpSetup(autoDownloadBinary = true) {
3772
4039
  __name(ensureYtDlpSetup, "ensureYtDlpSetup");
3773
4040
  async function findExistingBinary2(prefix, suffix) {
3774
4041
  try {
3775
- const files = await fs5.readdir(getBinDir());
4042
+ const files = await fs6.readdir(getBinDir());
3776
4043
  const binaries = files.filter((file) => file.startsWith(prefix) && file.endsWith(suffix));
3777
4044
  if (binaries.length === 0) {
3778
4045
  return null;
3779
4046
  }
3780
4047
  binaries.sort().reverse();
3781
- const binaryPath = path4.join(getBinDir(), binaries[0]);
3782
- await fs5.access(binaryPath);
4048
+ const binaryPath = path5.join(getBinDir(), binaries[0]);
4049
+ await fs6.access(binaryPath);
3783
4050
  return binaryPath;
3784
4051
  } catch (error) {
3785
4052
  globalLogger.warn(`Failed to find existing binary: ${error}`);
@@ -3814,10 +4081,10 @@ async function getLatestYtDlpVersion() {
3814
4081
  __name(getLatestYtDlpVersion, "getLatestYtDlpVersion");
3815
4082
  async function cleanupOldVersions2(prefix, currentVersion) {
3816
4083
  try {
3817
- const files = await fs5.readdir(getBinDir());
4084
+ const files = await fs6.readdir(getBinDir());
3818
4085
  const oldVersions = files.filter((file) => file.startsWith(prefix) && file.endsWith(".exe") && file !== currentVersion);
3819
4086
  for (const file of oldVersions) {
3820
- await fs5.unlink(path4.join(getBinDir(), file));
4087
+ await fs6.unlink(path5.join(getBinDir(), file));
3821
4088
  globalLogger.info(`Cleaned up old version: ${file}`);
3822
4089
  }
3823
4090
  } catch (error) {
@@ -3845,7 +4112,7 @@ async function downloadFile2(url, destination) {
3845
4112
  resolve();
3846
4113
  });
3847
4114
  fileStream.on("error", (error) => {
3848
- fs5.unlink(destination).catch(() => {
4115
+ fs6.unlink(destination).catch(() => {
3849
4116
  });
3850
4117
  reject(error);
3851
4118
  });
@@ -3858,12 +4125,12 @@ __name(downloadFile2, "downloadFile");
3858
4125
 
3859
4126
  // src/flows/musicDownloadFlow/services/download-providers/ytdlp/YtDlpCell.tsx
3860
4127
  import React15 from "react";
3861
- import path5 from "path";
4128
+ import path6 from "path";
3862
4129
  import { Text as Text11 } from "ink";
3863
4130
  var YtDlpCell = /* @__PURE__ */ __name(({ task, isSelected }) => {
3864
4131
  const downloadSource = task.attributes?.downloadSources.find((d) => d.provider === "ytdlp");
3865
4132
  const saved = downloadSource?.savedFile;
3866
- const display = saved ? path5.basename(saved.path) : downloadSource?.localFile?.name ?? downloadSource?.state ?? "";
4133
+ const display = saved ? path6.basename(saved.path) : downloadSource?.localFile?.name ?? downloadSource?.state ?? "";
3867
4134
  const color = saved ? "cyan" : downloadSource?.state === "downloaded" ? "green" : "white";
3868
4135
  return /* @__PURE__ */ React15.createElement(Text11, {
3869
4136
  color,
@@ -3872,91 +4139,6 @@ var YtDlpCell = /* @__PURE__ */ __name(({ task, isSelected }) => {
3872
4139
  }, display);
3873
4140
  }, "YtDlpCell");
3874
4141
 
3875
- // src/flows/musicDownloadFlow/saveSettings.ts
3876
- import fs6 from "fs";
3877
-
3878
- // src/flows/musicDownloadFlow/settings.ts
3879
- import * as os from "os";
3880
- import * as path6 from "path";
3881
- function extractProviderDefaults(registry) {
3882
- const result = {};
3883
- for (const key of registry.getFactories().keys()) {
3884
- const schema = registry.getConstructor(key)?.defaultSettings;
3885
- result[key] = schema ? Object.fromEntries(Object.entries(schema).map(([k, v]) => [
3886
- k,
3887
- v.defaultValue
3888
- ])) : {
3889
- enabled: true
3890
- };
3891
- }
3892
- return result;
3893
- }
3894
- __name(extractProviderDefaults, "extractProviderDefaults");
3895
- function getPlatformDefaultMusicOutputDir() {
3896
- switch (process.platform) {
3897
- case "win32":
3898
- return path6.join(os.homedir(), "Music", "GoblinMalin");
3899
- case "darwin":
3900
- return path6.join(os.homedir(), "Music", "GoblinMalin");
3901
- default:
3902
- return path6.join(process.env.XDG_MUSIC_DIR ?? path6.join(os.homedir(), "Music"), "GoblinMalin");
3903
- }
3904
- }
3905
- __name(getPlatformDefaultMusicOutputDir, "getPlatformDefaultMusicOutputDir");
3906
- var MEDIA_OUTPUT_DIR = getPlatformDefaultMusicOutputDir();
3907
- var BASE_DEFAULT_MUSIC_DOWNLOAD_FLOW_SETTINGS = {
3908
- metadata: {
3909
- autoChooseBestSource: false,
3910
- providers: {},
3911
- discoveryProviders: {}
3912
- },
3913
- download: {
3914
- autoChooseBestSource: false,
3915
- autoSaveToOutputDir: true,
3916
- autoDeleteTempAfter24h: false,
3917
- autoRelocateMissingFiles: false,
3918
- outputDir: MEDIA_OUTPUT_DIR,
3919
- outputTemporaryDir: path6.join(MEDIA_OUTPUT_DIR, "tmp"),
3920
- providers: {}
3921
- },
3922
- ui: {
3923
- columnRatios: {}
3924
- }
3925
- };
3926
-
3927
- // src/flows/musicDownloadFlow/saveSettings.ts
3928
- function getFlowSettings() {
3929
- return SettingsStore.getInstance().getFlowSettings("music-downloader", BASE_DEFAULT_MUSIC_DOWNLOAD_FLOW_SETTINGS);
3930
- }
3931
- __name(getFlowSettings, "getFlowSettings");
3932
- function getTempDownloadDir() {
3933
- return getFlowSettings().download.outputTemporaryDir;
3934
- }
3935
- __name(getTempDownloadDir, "getTempDownloadDir");
3936
- function getDownloadProviderSettings(providerKey) {
3937
- return getFlowSettings().download.providers[providerKey] ?? {};
3938
- }
3939
- __name(getDownloadProviderSettings, "getDownloadProviderSettings");
3940
- function clearTempDownloads() {
3941
- const dir = getTempDownloadDir();
3942
- if (fs6.existsSync(dir)) {
3943
- fs6.rmSync(dir, {
3944
- recursive: true,
3945
- force: true
3946
- });
3947
- }
3948
- globalLogger.info("Temporary downloads cleared");
3949
- }
3950
- __name(clearTempDownloads, "clearTempDownloads");
3951
- function getSaveSettings() {
3952
- const s = getFlowSettings();
3953
- return {
3954
- outputDir: s.download.outputDir,
3955
- includeMusicBrainzTags: false
3956
- };
3957
- }
3958
- __name(getSaveSettings, "getSaveSettings");
3959
-
3960
4142
  // src/flows/musicDownloadFlow/utils/tempFile.ts
3961
4143
  import { randomUUID } from "crypto";
3962
4144
  import * as fs7 from "fs";
@@ -5027,7 +5209,7 @@ var DownloadTask = class extends Task {
5027
5209
  }
5028
5210
  if (existingSavedPath) {
5029
5211
  if (existingSavedPath !== outputPath) {
5030
- const { moveFile } = await import("./metadata-K3LLTE6Z.js");
5212
+ const { moveFile } = await import("./metadata-4273SL44.js");
5031
5213
  await moveFile(existingSavedPath, outputPath);
5032
5214
  }
5033
5215
  outputCreated = true;
@@ -9088,8 +9270,40 @@ var useActiveWizardPrompt = /* @__PURE__ */ __name((tasks) => {
9088
9270
  }, "useActiveWizardPrompt");
9089
9271
 
9090
9272
  // src/components/SetupWizardModal/SetupWizardModal.tsx
9091
- function buildInteractiveItems(config) {
9273
+ function buildInteractiveItems(config, selectedMode) {
9092
9274
  const items = [];
9275
+ if (config.modes) {
9276
+ for (const mode of config.modes) {
9277
+ items.push({
9278
+ kind: "mode",
9279
+ id: mode.id,
9280
+ label: mode.label
9281
+ });
9282
+ }
9283
+ const activeMode = config.modes.find((m) => m.id === selectedMode) ?? config.modes[0];
9284
+ for (const block of activeMode.details ?? []) {
9285
+ if (block.type === "orderedList") {
9286
+ for (const item of block.items) {
9287
+ if (item.type === "link") {
9288
+ items.push({
9289
+ kind: "link",
9290
+ text: item.text,
9291
+ url: item.url
9292
+ });
9293
+ }
9294
+ }
9295
+ }
9296
+ }
9297
+ for (const field of activeMode.fields) {
9298
+ items.push({
9299
+ kind: "field",
9300
+ envVar: field.envVar,
9301
+ label: field.label,
9302
+ hint: field.hint
9303
+ });
9304
+ }
9305
+ return items;
9306
+ }
9093
9307
  for (const block of config.description) {
9094
9308
  if (block.type === "orderedList") {
9095
9309
  for (const item of block.items) {
@@ -9132,26 +9346,41 @@ var SetupWizardModal = /* @__PURE__ */ __name(({ tasks, terminalHeight, terminal
9132
9346
  const [focusedIndex, setFocusedIndex] = useState15(0);
9133
9347
  const [fieldValues, setFieldValues] = useState15({});
9134
9348
  const [editingField, setEditingField] = useState15(null);
9349
+ const [selectedMode, setSelectedMode] = useState15(() => {
9350
+ if (config?.modes && config.modeEnvVar) {
9351
+ return process.env[config.modeEnvVar] ?? config.modes[0].id;
9352
+ }
9353
+ return "";
9354
+ });
9135
9355
  const [prevConfig, setPrevConfig] = useState15(config);
9136
9356
  if (prevConfig !== config) {
9137
9357
  setPrevConfig(config);
9138
9358
  if (config) {
9139
9359
  setFocusedIndex(0);
9140
9360
  setEditingField(null);
9141
- setFieldValues(Object.fromEntries(config.fields.map((f) => [
9361
+ const initialMode = config.modes && config.modeEnvVar ? process.env[config.modeEnvVar] ?? config.modes[0].id : "";
9362
+ setSelectedMode(initialMode);
9363
+ const activeFields2 = config.modes ? (config.modes.find((m) => m.id === initialMode) ?? config.modes[0]).fields : config.fields;
9364
+ setFieldValues(Object.fromEntries(activeFields2.map((f) => [
9142
9365
  f.envVar,
9143
9366
  process.env[f.envVar] ?? ""
9144
9367
  ])));
9145
9368
  }
9146
9369
  }
9147
- const interactiveItems = useMemo3(() => config ? buildInteractiveItems(config) : [], [
9148
- config
9370
+ const interactiveItems = useMemo3(() => config ? buildInteractiveItems(config, selectedMode) : [], [
9371
+ config,
9372
+ selectedMode
9149
9373
  ]);
9150
9374
  const handleSubmit = useCallback3(async () => {
9151
9375
  if (!config) return;
9376
+ const activeFields2 = config.modes ? (config.modes.find((m) => m.id === selectedMode) ?? config.modes[0]).fields : config.fields;
9152
9377
  const nonEmpty = {};
9153
9378
  const toRemove = [];
9154
- for (const field of config.fields) {
9379
+ if (config.modes && config.modeEnvVar) {
9380
+ process.env[config.modeEnvVar] = selectedMode;
9381
+ nonEmpty[config.modeEnvVar] = selectedMode;
9382
+ }
9383
+ for (const field of activeFields2) {
9155
9384
  const value = fieldValues[field.envVar] ?? "";
9156
9385
  if (value.trim()) {
9157
9386
  process.env[field.envVar] = value;
@@ -9171,16 +9400,24 @@ var SetupWizardModal = /* @__PURE__ */ __name(({ tasks, terminalHeight, terminal
9171
9400
  await saveEnvVar(key, value);
9172
9401
  }
9173
9402
  }
9403
+ const resolvedValues = {};
9404
+ if (config.modes && config.modeEnvVar) {
9405
+ resolvedValues[config.modeEnvVar] = selectedMode;
9406
+ }
9407
+ for (const field of activeFields2) {
9408
+ resolvedValues[field.envVar] = fieldValues[field.envVar] ?? "";
9409
+ }
9174
9410
  if (wizardTask && wizardPrompt) {
9175
9411
  const current = wizardPrompt.getCurrentPrompt();
9176
9412
  if (current?.type === PromptType.SetupWizard) {
9177
- wizardPrompt.resolvePrompt(fieldValues);
9413
+ wizardPrompt.resolvePrompt(resolvedValues);
9178
9414
  }
9179
9415
  }
9180
9416
  switchBack();
9181
9417
  }, [
9182
9418
  config,
9183
9419
  fieldValues,
9420
+ selectedMode,
9184
9421
  wizardTask,
9185
9422
  wizardPrompt,
9186
9423
  switchBack
@@ -9280,6 +9517,20 @@ var SetupWizardModal = /* @__PURE__ */ __name(({ tasks, terminalHeight, terminal
9280
9517
  if (!item) return;
9281
9518
  if (item.kind === "link") openUrl(item.url);
9282
9519
  else if (item.kind === "field") setEditingField(item.envVar);
9520
+ else if (item.kind === "mode") {
9521
+ const newMode = item.id;
9522
+ setSelectedMode(newMode);
9523
+ if (config?.modes) {
9524
+ const newActiveMode = config.modes.find((m) => m.id === newMode) ?? config.modes[0];
9525
+ setFieldValues((prev) => ({
9526
+ ...prev,
9527
+ ...Object.fromEntries(newActiveMode.fields.map((f) => [
9528
+ f.envVar,
9529
+ process.env[f.envVar] ?? ""
9530
+ ]))
9531
+ }));
9532
+ }
9533
+ }
9283
9534
  }, "handler")
9284
9535
  }
9285
9536
  ]
@@ -9287,31 +9538,13 @@ var SetupWizardModal = /* @__PURE__ */ __name(({ tasks, terminalHeight, terminal
9287
9538
  if (!isActive || !config) return null;
9288
9539
  const borderColor = config.providerKey ? providerDisplayRegistry.get(config.providerKey).color : theme.ui.modalBorder;
9289
9540
  const modalWidth = Math.min(80, Math.max(60, terminalWidth - 6));
9290
- const labelWidth = Math.max(...config.fields.map((f) => f.label.length)) + 2;
9291
- let linkInteractiveIndex = 0;
9292
- return /* @__PURE__ */ React29.createElement(Box15, {
9293
- position: "absolute",
9294
- width: "100%",
9295
- height: terminalHeight,
9296
- flexDirection: "column",
9297
- justifyContent: "center",
9298
- alignItems: "center"
9299
- }, /* @__PURE__ */ React29.createElement(Box15, {
9300
- flexDirection: "column",
9301
- borderStyle: "round",
9302
- borderColor,
9303
- borderBackgroundColor: theme.ui.background,
9304
- paddingX: 2,
9305
- paddingY: 1,
9306
- width: modalWidth,
9307
- backgroundColor: theme.ui.background
9308
- }, /* @__PURE__ */ React29.createElement(Text25, {
9309
- bold: true,
9310
- color: borderColor
9311
- }, config.title), /* @__PURE__ */ React29.createElement(Box15, {
9312
- flexDirection: "column",
9313
- marginTop: 1
9314
- }, config.description.map((block, bi) => {
9541
+ const activeMode = config.modes ? config.modes.find((m) => m.id === selectedMode) ?? config.modes[0] : void 0;
9542
+ const activeFields = activeMode ? activeMode.fields : config.fields;
9543
+ const labelWidth = activeFields.length > 0 ? Math.max(...activeFields.map((f) => f.label.length)) + 2 : 2;
9544
+ const activeModeDescription = activeMode?.description;
9545
+ const activeModeDetails = activeMode?.details;
9546
+ let linkInteractiveIndex = config.modes ? config.modes.length : 0;
9547
+ const renderContentBlocks = /* @__PURE__ */ __name((blocks) => blocks.map((block, bi) => {
9315
9548
  if (block.type === "note") {
9316
9549
  return /* @__PURE__ */ React29.createElement(Box15, {
9317
9550
  key: bi,
@@ -9361,17 +9594,74 @@ var SetupWizardModal = /* @__PURE__ */ __name(({ tasks, terminalHeight, terminal
9361
9594
  }));
9362
9595
  }
9363
9596
  return null;
9364
- })), /* @__PURE__ */ React29.createElement(Box15, {
9597
+ }), "renderContentBlocks");
9598
+ return /* @__PURE__ */ React29.createElement(Box15, {
9599
+ position: "absolute",
9600
+ width: "100%",
9601
+ height: terminalHeight,
9602
+ flexDirection: "column",
9603
+ justifyContent: "center",
9604
+ alignItems: "center"
9605
+ }, /* @__PURE__ */ React29.createElement(Box15, {
9606
+ flexDirection: "column",
9607
+ borderStyle: "round",
9608
+ borderColor,
9609
+ borderBackgroundColor: theme.ui.background,
9610
+ paddingX: 2,
9611
+ paddingY: 1,
9612
+ width: modalWidth,
9613
+ backgroundColor: theme.ui.background
9614
+ }, /* @__PURE__ */ React29.createElement(Text25, {
9615
+ bold: true,
9616
+ color: borderColor
9617
+ }, config.title), config.modes && /* @__PURE__ */ React29.createElement(Box15, {
9618
+ flexDirection: "column",
9619
+ marginTop: 1
9620
+ }, /* @__PURE__ */ React29.createElement(Text25, {
9621
+ color: theme.text.heading,
9622
+ bold: true
9623
+ }, "Choose how to fetch Spotify metadata:"), config.modes.map((mode) => {
9624
+ const itemIndex = interactiveItems.findIndex((it) => it.kind === "mode" && it.id === mode.id);
9625
+ const isFocused = focusedIndex === itemIndex;
9626
+ const isSelected = selectedMode === mode.id;
9627
+ const itemColor = isFocused ? theme.action.primary : isSelected ? borderColor : theme.text.secondary;
9628
+ return /* @__PURE__ */ React29.createElement(Box15, {
9629
+ key: mode.id,
9630
+ flexDirection: "row"
9631
+ }, /* @__PURE__ */ React29.createElement(Text25, {
9632
+ color: isFocused ? theme.action.primary : theme.text.muted
9633
+ }, isFocused ? "\u261B " : " "), /* @__PURE__ */ React29.createElement(Text25, {
9634
+ color: itemColor,
9635
+ bold: isSelected
9636
+ }, isSelected ? "\u25C9 " : "\u25CB "), /* @__PURE__ */ React29.createElement(Text25, {
9637
+ color: itemColor,
9638
+ bold: isSelected || isFocused
9639
+ }, mode.label));
9640
+ }), activeModeDescription && /* @__PURE__ */ React29.createElement(Box15, {
9641
+ marginTop: 1,
9642
+ paddingLeft: 4
9643
+ }, /* @__PURE__ */ React29.createElement(Text25, {
9644
+ color: theme.text.secondary,
9645
+ italic: true
9646
+ }, activeModeDescription)), activeModeDetails && activeModeDetails.length > 0 && /* @__PURE__ */ React29.createElement(Box15, {
9647
+ flexDirection: "column",
9648
+ marginTop: 1,
9649
+ paddingLeft: 4
9650
+ }, renderContentBlocks(activeModeDetails))), !config.modes && /* @__PURE__ */ React29.createElement(Box15, {
9365
9651
  flexDirection: "column",
9366
9652
  marginTop: 1
9367
- }, config.fields.map((field, fi) => {
9653
+ }, renderContentBlocks(config.description)), /* @__PURE__ */ React29.createElement(Box15, {
9654
+ flexDirection: "column",
9655
+ marginTop: 1,
9656
+ paddingLeft: config.modes ? 2 : 0
9657
+ }, activeFields.map((field, fi) => {
9368
9658
  const itemIndex = interactiveItems.findIndex((it) => it.kind === "field" && it.envVar === field.envVar);
9369
9659
  const isFocused = focusedIndex === itemIndex;
9370
9660
  const isEditing = editingField === field.envVar;
9371
9661
  return /* @__PURE__ */ React29.createElement(Box15, {
9372
9662
  flexDirection: "row",
9373
9663
  key: field.envVar,
9374
- marginTop: fi === 0 ? 1 : 0,
9664
+ marginTop: fi === 0 ? config.modes ? 0 : 1 : 0,
9375
9665
  height: 3,
9376
9666
  alignItems: "center"
9377
9667
  }, /* @__PURE__ */ React29.createElement(Text25, {
@@ -9615,10 +9905,10 @@ import { spawn as spawn3, exec as exec2 } from "child_process";
9615
9905
  import open2 from "open";
9616
9906
 
9617
9907
  // src/updater/installSource.ts
9618
- import { createRequire } from "module";
9908
+ import { createRequire as createRequire2 } from "module";
9619
9909
  import { readFileSync as readFileSync2 } from "fs";
9620
9910
  import { join as join10 } from "path";
9621
- var _require = createRequire(import.meta.url);
9911
+ var _require = createRequire2(import.meta.url);
9622
9912
  var IS_SEA = (() => {
9623
9913
  try {
9624
9914
  const sea = _require("node:sea");
@@ -109,7 +109,7 @@ dotenv.config({
109
109
  });
110
110
  function resolveAppVersion() {
111
111
  try {
112
- return "0.1.13";
112
+ return "0.1.14";
113
113
  } catch {
114
114
  const _req = createRequire(import.meta.url);
115
115
  return _req("../package.json").version;
@@ -121,9 +121,9 @@ var APP_VERSION = resolveAppVersion();
121
121
  // src/settings/appSettings.ts
122
122
  var DEFAULT_APP_SETTINGS = {
123
123
  general: {
124
- reopenLastSession: false,
124
+ reopenLastSession: true,
125
125
  appDataDir: DEFAULT_APP_DATA_DIR,
126
- animationsEnabled: false,
126
+ animationsEnabled: true,
127
127
  theme: "dark",
128
128
  showWelcomeTutorial: true,
129
129
  checkForUpdates: true
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  start
4
- } from "./chunk-OUQ2CB5Q.js";
5
- import "./chunk-4NSWYXSP.js";
4
+ } from "./chunk-5KBWKKXR.js";
5
+ import "./chunk-QSUGRYOC.js";
6
6
 
7
7
  // src/cli.ts
8
8
  start();
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  src_default,
3
3
  start,
4
4
  withFullScreen
5
- } from "./chunk-OUQ2CB5Q.js";
6
- import "./chunk-4NSWYXSP.js";
5
+ } from "./chunk-5KBWKKXR.js";
6
+ import "./chunk-QSUGRYOC.js";
7
7
  export {
8
8
  src_default as default,
9
9
  start,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanAndTagFlac,
3
3
  moveFile
4
- } from "./chunk-4NSWYXSP.js";
4
+ } from "./chunk-QSUGRYOC.js";
5
5
  export {
6
6
  cleanAndTagFlac,
7
7
  moveFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goblin-malin",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A keyboard-driven terminal UI for downloading and tagging music tracks with metadata from Spotify and YouTube",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -103,7 +103,6 @@
103
103
  "eslint-plugin-react-hooks": "^7.1.1",
104
104
  "postject": "^1.0.0-alpha.6",
105
105
  "prettier": "^3.8.3",
106
- "spotify-url-info": "^3.3.0",
107
106
  "ts-node": "^10.9.2",
108
107
  "tsup": "^8.0.0",
109
108
  "tsx": "^4.21.0",
@@ -134,6 +133,7 @@
134
133
  "open": "^11.0.0",
135
134
  "react": "^19.2.6",
136
135
  "slsk-client": "^1.1.0",
136
+ "spotify-url-info": "^3.3.0",
137
137
  "winston": "^3.19.0",
138
138
  "winston-transport": "^4.9.0",
139
139
  "ytdlp-nodejs": "^3.4.4",