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-
|
|
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
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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: "
|
|
2328
|
-
text: "
|
|
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: "
|
|
2333
|
-
text:
|
|
2520
|
+
type: "paragraph",
|
|
2521
|
+
text: "Steps to setup Spotify API access:"
|
|
2334
2522
|
},
|
|
2335
2523
|
{
|
|
2336
|
-
type: "
|
|
2337
|
-
|
|
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
|
-
|
|
2350
|
-
label: "
|
|
2351
|
-
|
|
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
|
|
2569
|
+
async resolveAuth() {
|
|
2361
2570
|
return this.runExclusive("init", async () => {
|
|
2362
|
-
if (
|
|
2363
|
-
|
|
2364
|
-
|
|
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
|
|
2470
|
-
if (
|
|
2471
|
-
|
|
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
|
|
3222
|
+
import path3 from "path";
|
|
2956
3223
|
|
|
2957
3224
|
// src/flows/musicDownloadFlow/services/metadata-providers/songlink/json.ts
|
|
2958
|
-
import
|
|
3225
|
+
import fs4 from "fs/promises";
|
|
2959
3226
|
async function ensureCacheDir() {
|
|
2960
3227
|
try {
|
|
2961
|
-
await
|
|
3228
|
+
await fs4.access(getCacheDir());
|
|
2962
3229
|
} catch {
|
|
2963
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
3398
|
+
__name(parseArtistNames2, "parseArtistNames");
|
|
3132
3399
|
function convertSonglinkEntityToTrackMetadata(platformKey, platformLink, entity) {
|
|
3133
3400
|
if (!SUPPORTED_PLATFORMS.has(platformKey)) return null;
|
|
3134
|
-
const artistNames =
|
|
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
|
|
3550
|
+
const api2 = this.getApi();
|
|
3284
3551
|
const { isrc, trackName, artistName } = params;
|
|
3285
3552
|
if (isrc) {
|
|
3286
|
-
const byIsrc = await
|
|
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
|
|
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
|
|
3819
|
+
import * as fs5 from "fs/promises";
|
|
3553
3820
|
import { createWriteStream } from "fs";
|
|
3554
|
-
import * as
|
|
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 =
|
|
3846
|
+
const binaryPath = path4.join(getBinDir(), binaryName);
|
|
3580
3847
|
try {
|
|
3581
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
3604
|
-
await
|
|
3605
|
-
await
|
|
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
|
|
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 =
|
|
3627
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3989
|
+
import * as fs6 from "fs/promises";
|
|
3723
3990
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
3724
|
-
import * as
|
|
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 =
|
|
4014
|
+
const binaryPath = path5.join(getBinDir(), binaryName);
|
|
3748
4015
|
try {
|
|
3749
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
3782
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
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
|
-
})
|
|
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.
|
|
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 =
|
|
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.
|
|
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:
|
|
124
|
+
reopenLastSession: true,
|
|
125
125
|
appDataDir: DEFAULT_APP_DATA_DIR,
|
|
126
|
-
animationsEnabled:
|
|
126
|
+
animationsEnabled: true,
|
|
127
127
|
theme: "dark",
|
|
128
128
|
showWelcomeTutorial: true,
|
|
129
129
|
checkForUpdates: true
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goblin-malin",
|
|
3
|
-
"version": "0.1.
|
|
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",
|