mr-magic-mcp-server 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -20
- package/README.md +28 -23
- package/package.json +2 -2
- package/src/providers/musixmatch.js +2 -2
- package/src/transport/mcp-http-server.js +5 -0
- package/src/transport/mcp-tools.js +1 -1
- package/src/transport/token-startup-log.js +6 -3
- package/src/utils/tokens/genius-token-manager.js +80 -5
- package/src/utils/tokens/musixmatch-token-manager.js +7 -7
package/.env.example
CHANGED
|
@@ -3,13 +3,27 @@
|
|
|
3
3
|
# At minimum one of these is needed for Genius lyrics support.
|
|
4
4
|
# Get all three from https://genius.com/api-clients
|
|
5
5
|
# ───────────────────────────────────────────────────────────────────────────────
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# There are three ways to supply the Genius token (tried in order):
|
|
7
|
+
#
|
|
8
|
+
# Auto-refresh (recommended for all deployments, including Render/ephemeral)
|
|
9
|
+
# Set GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET. The server calls the Genius
|
|
10
|
+
# OAuth client_credentials endpoint at runtime and refreshes the token in
|
|
11
|
+
# memory automatically. No disk, no scripts, no manual token copying needed.
|
|
12
|
+
#
|
|
13
|
+
# Fallback token (env var) — static bearer token, no auto-refresh
|
|
14
|
+
# Set GENIUS_ACCESS_TOKEN. Works everywhere but the token must be
|
|
15
|
+
# manually updated on expiry (or redeploy with a new value).
|
|
16
|
+
# Use this only when client_credentials are unavailable.
|
|
17
|
+
#
|
|
18
|
+
# Cache token (on-disk) — local development only
|
|
19
|
+
# Run `npm run fetch:genius-token` to write a token to
|
|
20
|
+
# `.cache/genius-token.json`. The server reads it on startup when a
|
|
21
|
+
# persistent, writable filesystem is available. Not suitable for
|
|
22
|
+
# ephemeral hosts.
|
|
9
23
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
GENIUS_CLIENT_ID= # Auto-refresh: OAuth client ID (enables runtime auto-refresh)
|
|
25
|
+
GENIUS_CLIENT_SECRET= # Auto-refresh: OAuth client secret (enables runtime auto-refresh)
|
|
26
|
+
GENIUS_ACCESS_TOKEN= # Fallback token: static bearer token (required if not using client credentials)
|
|
13
27
|
|
|
14
28
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
15
29
|
# REQUIRED (feature-specific) — Musixmatch
|
|
@@ -18,20 +32,19 @@ GENIUS_CLIENT_SECRET= # OAuth client secret (enables auto-refresh)
|
|
|
18
32
|
# ───────────────────────────────────────────────────────────────────────────────
|
|
19
33
|
# There are two ways to supply the Musixmatch token:
|
|
20
34
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
35
|
+
# Fallback token (env var) — recommended for production / ephemeral hosts
|
|
36
|
+
# Set MUSIXMATCH_FALLBACK_TOKEN or MUSIXMATCH_ALT_FALLBACK_TOKEN as an environment variable.
|
|
37
|
+
# Use this when persistent filesystem storage is not available (e.g. Render
|
|
38
|
+
# free tier, serverless, containers without a mounted volume).
|
|
39
|
+
# Resolution order: MUSIXMATCH_FALLBACK_TOKEN (1st) → MUSIXMATCH_ALT_FALLBACK_TOKEN (2nd)
|
|
26
40
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
41
|
+
# Cache token (on-disk) — local development only
|
|
42
|
+
# Run `npm run fetch:musixmatch-token` to sign in via Playwright and write
|
|
43
|
+
# the token to `.cache/musixmatch-token.json`. The server reads it on startup.
|
|
44
|
+
# Not suitable for ephemeral hosts where the filesystem is wiped on restart.
|
|
31
45
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
MUSIXMATCH_FALLBACK_TOKEN= # Fallback token (1st priority) — set this in production
|
|
47
|
+
MUSIXMATCH_ALT_FALLBACK_TOKEN= # Fallback token (2nd priority) — alternative env var
|
|
35
48
|
|
|
36
49
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
37
50
|
# REQUIRED (feature-specific) — Airtable
|
|
@@ -91,8 +104,9 @@ MR_MAGIC_INLINE_PAYLOAD_MAX_CHARS=1500
|
|
|
91
104
|
# Export TTL in seconds for local and redis backends (default: 3600 / 1 hour)
|
|
92
105
|
MR_MAGIC_EXPORT_TTL_SECONDS=3600
|
|
93
106
|
|
|
94
|
-
#
|
|
95
|
-
|
|
107
|
+
# Override the on-disk cache token paths (local dev only)
|
|
108
|
+
GENIUS_TOKEN_CACHE=.cache/genius-token.json
|
|
109
|
+
MUSIXMATCH_TOKEN_CACHE=.cache/musixmatch-token.json
|
|
96
110
|
|
|
97
111
|
# Musixmatch: when 1, provider will attempt to re-run the fetch script
|
|
98
112
|
# automatically (headless) if no token is available
|
package/README.md
CHANGED
|
@@ -84,10 +84,10 @@ MR_MAGIC_ENV_PATH= # Optional. Custom path to an env file when the default isn't
|
|
|
84
84
|
GENIUS_CLIENT_ID= # Get from https://genius.com/api-clients, required for Genius client-credentials auth.
|
|
85
85
|
GENIUS_CLIENT_SECRET= # Get from https://genius.com/api-clients, required for Genius client-credentials auth.
|
|
86
86
|
GENIUS_ACCESS_TOKEN= # Get from https://genius.com/api-clients, required for Genius lyrics support when client credentials are not supplied.
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
MUSIXMATCH_FALLBACK_TOKEN= # Fallback token (1st priority). Set as env var for production/ephemeral hosts where the filesystem is not persistent.
|
|
88
|
+
MUSIXMATCH_ALT_FALLBACK_TOKEN= # Fallback token (2nd priority). Alternative env var; same token value, second-choice source.
|
|
89
89
|
MUSIXMATCH_AUTO_FETCH=0 # Optional. When 1, provider will attempt to re-run the fetch script automatically (headless) if no token is available.
|
|
90
|
-
|
|
90
|
+
MUSIXMATCH_TOKEN_CACHE=.cache/musixmatch-token.json
|
|
91
91
|
MELON_COOKIE= # Optional. Pin a session cookie for consistent Melon results; anonymous access generally works without it.
|
|
92
92
|
MR_MAGIC_EXPORT_BACKEND= # local|inline|redis
|
|
93
93
|
MR_MAGIC_EXPORT_DIR=/absolute/path/to/exports # Required if MR_MAGIC_EXPORT_BACKEND=local
|
|
@@ -105,25 +105,31 @@ UPSTASH_REDIS_REST_TOKEN= # Get from https://console.upstash.com/redis/rest, req
|
|
|
105
105
|
AIRTABLE_PERSONAL_ACCESS_TOKEN= # Required for push_catalog_to_airtable tool. Get from https://airtable.com/create/tokens
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
- **
|
|
109
|
-
|
|
110
|
-
`GENIUS_CLIENT_ID
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
- **Genius token sources** — the server resolves the Genius token using three
|
|
109
|
+
sources (tried in order):
|
|
110
|
+
- **Auto-refresh** (`GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET`) — the server calls the
|
|
111
|
+
Genius OAuth `client_credentials` endpoint at runtime and keeps the token refreshed
|
|
112
|
+
in memory automatically. **Recommended for all deployments**, including Render/ephemeral
|
|
113
|
+
hosts. No disk, no scripts, no manual token copying.
|
|
114
|
+
- **Fallback token** (`GENIUS_ACCESS_TOKEN` env var) — a static bearer token. Works
|
|
115
|
+
everywhere but does not auto-refresh. Use only when client_credentials are unavailable;
|
|
116
|
+
redeploy with a new token when it expires.
|
|
117
|
+
- **Cache token** (on-disk `.cache/genius-token.json`) — written by
|
|
118
|
+
`npm run fetch:genius-token`. Loaded on startup when a persistent writable filesystem
|
|
119
|
+
is available. Not suitable for ephemeral hosts.
|
|
114
120
|
- **Musixmatch token sources** — the server resolves the Musixmatch token using
|
|
115
121
|
two named sources, tried in order:
|
|
116
|
-
- **Fallback token** (`
|
|
122
|
+
- **Fallback token** (`MUSIXMATCH_FALLBACK_TOKEN`, then `MUSIXMATCH_ALT_FALLBACK_TOKEN`) — the
|
|
117
123
|
token value is set directly as an environment variable. This is the
|
|
118
124
|
recommended approach for production and ephemeral hosts (e.g. Render free
|
|
119
125
|
tier, containers) where the filesystem cannot be relied upon between
|
|
120
|
-
restarts. Set `
|
|
126
|
+
restarts. Set `MUSIXMATCH_FALLBACK_TOKEN` first; `MUSIXMATCH_ALT_FALLBACK_TOKEN` is the
|
|
121
127
|
legacy/alternative env var for the same value.
|
|
122
128
|
- **Cache token** (on-disk `.cache/musixmatch-token.json`) — written by the
|
|
123
129
|
`fetch:musixmatch-token` script after a browser sign-in. Used for local
|
|
124
130
|
development when a persistent writable filesystem is available. Not suitable
|
|
125
131
|
for ephemeral hosts.
|
|
126
|
-
- **
|
|
132
|
+
- **MUSIXMATCH_TOKEN_CACHE** controls where the on-disk cache token file is
|
|
127
133
|
read/written (default `<project root>/.cache/musixmatch-token.json`).
|
|
128
134
|
- **MELON_COOKIE** is optional—anonymous access generally works, but pinning a
|
|
129
135
|
cookie can improve consistency.
|
|
@@ -188,7 +194,7 @@ in one of two ways depending on your deployment:
|
|
|
188
194
|
`.cache/musixmatch-token.json`. The server loads it on startup whenever a
|
|
189
195
|
persistent, writable filesystem is available.
|
|
190
196
|
- **Fallback token** (production/ephemeral): copy the captured token value and
|
|
191
|
-
set it as `
|
|
197
|
+
set it as `MUSIXMATCH_FALLBACK_TOKEN` (recommended) or `MUSIXMATCH_ALT_FALLBACK_TOKEN` in your
|
|
192
198
|
platform's environment. This is the only reliable option on ephemeral hosts
|
|
193
199
|
(Render free tier, containers without a mounted volume) where the filesystem
|
|
194
200
|
is wiped between restarts.
|
|
@@ -214,7 +220,7 @@ in one of two ways depending on your deployment:
|
|
|
214
220
|
the session.
|
|
215
221
|
|
|
216
222
|
4. **For remote/ephemeral deployments:** copy the `token` value from the
|
|
217
|
-
printed JSON and set it as `
|
|
223
|
+
printed JSON and set it as `MUSIXMATCH_FALLBACK_TOKEN` in your platform
|
|
218
224
|
environment (the fallback token). Do **not** rely on the cache file
|
|
219
225
|
surviving a restart on ephemeral hosts.
|
|
220
226
|
If `MUSIXMATCH_AUTO_FETCH=1`, the provider can attempt to re-run the fetch
|
|
@@ -224,7 +230,7 @@ in one of two ways depending on your deployment:
|
|
|
224
230
|
#### Developer Accounts
|
|
225
231
|
|
|
226
232
|
1. Get API access from `https://developer.musixmatch.com`
|
|
227
|
-
2. Run the script above and set the resulting token as `
|
|
233
|
+
2. Run the script above and set the resulting token as `MUSIXMATCH_FALLBACK_TOKEN`
|
|
228
234
|
(fallback token) in your environment, or keep the on-disk cache token in sync
|
|
229
235
|
for local development.
|
|
230
236
|
|
|
@@ -233,7 +239,7 @@ in one of two ways depending on your deployment:
|
|
|
233
239
|
1. Visit `https://auth.musixmatch.com/`
|
|
234
240
|
2. Sign in with a Musixmatch account and allow the app. When redirected, the
|
|
235
241
|
helper script above will capture the session and write the cache token.
|
|
236
|
-
3. Copy the `token` value and set it as `
|
|
242
|
+
3. Copy the `token` value and set it as `MUSIXMATCH_FALLBACK_TOKEN` for any remote
|
|
237
243
|
environment that needs it.
|
|
238
244
|
|
|
239
245
|
**WARNING: CALLING THE API FROM AN UNAUTHORIZED ACCOUNT MAY RESULT IN A BAN.**
|
|
@@ -594,7 +600,7 @@ Add env vars inline if your client supports the `env` field:
|
|
|
594
600
|
"args": ["-y", "mr-magic-mcp-server"],
|
|
595
601
|
"env": {
|
|
596
602
|
"GENIUS_ACCESS_TOKEN": "...",
|
|
597
|
-
"
|
|
603
|
+
"MUSIXMATCH_FALLBACK_TOKEN": "...",
|
|
598
604
|
"AIRTABLE_PERSONAL_ACCESS_TOKEN": "..."
|
|
599
605
|
}
|
|
600
606
|
}
|
|
@@ -1118,13 +1124,12 @@ For direct binary usage, use `mrmagic-cli search --artist ... --title ...`.
|
|
|
1118
1124
|
## Provider notes
|
|
1119
1125
|
|
|
1120
1126
|
- **LRCLIB**: Public API with synced lyric coverage; no auth required.
|
|
1121
|
-
- **Genius**: Requires `
|
|
1122
|
-
|
|
1127
|
+
- **Genius**: Requires credentials — either `GENIUS_CLIENT_ID` + `GENIUS_CLIENT_SECRET`
|
|
1128
|
+
for auto-refresh (recommended) or `GENIUS_ACCESS_TOKEN` as a static fallback token.
|
|
1123
1129
|
- **Musixmatch**: Requires a token — either a **fallback token** set via
|
|
1124
|
-
`
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
token" above for the full workflow.
|
|
1130
|
+
`MUSIXMATCH_FALLBACK_TOKEN` (recommended for production) or `MUSIXMATCH_ALT_FALLBACK_TOKEN` env var,
|
|
1131
|
+
or a **cache token** written to disk by `npm run fetch:musixmatch-token` (local dev).
|
|
1132
|
+
See "Getting the Musixmatch token" above for the full workflow.
|
|
1128
1133
|
- **Melon**: Works anonymously but benefits from `MELON_COOKIE` for reliability
|
|
1129
1134
|
if needed.
|
|
1130
1135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mr-magic-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Lyrics MCP server connecting LRCLIB, Genius, Musixmatch, and Melon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"format": "prettier --write .",
|
|
31
31
|
"format:check": "prettier --check .",
|
|
32
32
|
"test": "node tests/run-tests.js",
|
|
33
|
-
"fetch:musixmatch-token": "node scripts/
|
|
33
|
+
"fetch:musixmatch-token": "node scripts/fetch_MUSIXMATCH_ALT_FALLBACK_TOKEN.mjs",
|
|
34
34
|
"fetch:genius-token": "node scripts/fetch_genius_token.mjs",
|
|
35
35
|
"repro:mcp:arg-boundary": "node scripts/mcp-arg-boundary-repro.mjs",
|
|
36
36
|
"repro:mcp:arg-boundary:sdk": "node scripts/mcp-arg-boundary-sdk-repro.mjs",
|
|
@@ -138,11 +138,11 @@ async function macroRequest(track) {
|
|
|
138
138
|
async function ensureMusixmatchToken() {
|
|
139
139
|
const token = await getMusixmatchToken();
|
|
140
140
|
if (!token) {
|
|
141
|
-
// Neither a fallback token (
|
|
141
|
+
// Neither a fallback token (MUSIXMATCH_FALLBACK_TOKEN / MUSIXMATCH_ALT_USER_TOKEN env vars) nor a
|
|
142
142
|
// cache token (on-disk .cache/musixmatch-token.json) could be found.
|
|
143
143
|
throw new Error(
|
|
144
144
|
'Musixmatch token not found. ' +
|
|
145
|
-
'Set
|
|
145
|
+
'Set MUSIXMATCH_FALLBACK_TOKEN (fallback token — recommended for production/ephemeral hosts) ' +
|
|
146
146
|
'or MUSIXMATCH_ALT_USER_TOKEN as an environment variable, ' +
|
|
147
147
|
'or run `npm run fetch:musixmatch-token` to populate the on-disk cache token.'
|
|
148
148
|
);
|
|
@@ -13,6 +13,7 @@ import { mcpToolDefinitions, handleMcpTool } from './mcp-tools.js';
|
|
|
13
13
|
import { buildMcpResponse } from './mcp-response.js';
|
|
14
14
|
import { logTokenStatus } from './token-startup-log.js';
|
|
15
15
|
import { normalizeToolArgs } from './tool-args.js';
|
|
16
|
+
import { getProviderStatus } from '../index.js';
|
|
16
17
|
|
|
17
18
|
function getBodyShape(body) {
|
|
18
19
|
if (body == null) return 'nullish';
|
|
@@ -91,6 +92,10 @@ export async function startMcpHttpServer(options = {}) {
|
|
|
91
92
|
await logTokenStatus({ context: 'http-mcp' });
|
|
92
93
|
|
|
93
94
|
const app = createMcpExpressApp({ host });
|
|
95
|
+
app.get('/health', async (_req, res) => {
|
|
96
|
+
res.json({ status: 'ok', providers: await getProviderStatus() });
|
|
97
|
+
});
|
|
98
|
+
|
|
94
99
|
app.all('/mcp', async (req, res) => {
|
|
95
100
|
const normalizedBody = normalizeIncomingRpcBody(req.body);
|
|
96
101
|
const requestId = randomUUID();
|
|
@@ -29,7 +29,7 @@ export async function logTokenStatus({ context }) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function logGeniusStatus(context) {
|
|
32
|
-
const diagnostics = getGeniusDiagnostics();
|
|
32
|
+
const diagnostics = await getGeniusDiagnostics();
|
|
33
33
|
const ready = hasValidGeniusAuth();
|
|
34
34
|
const mode = describeGeniusAuthMode();
|
|
35
35
|
|
|
@@ -37,7 +37,9 @@ async function logGeniusStatus(context) {
|
|
|
37
37
|
context,
|
|
38
38
|
provider: 'genius',
|
|
39
39
|
clientCredentialsPresent: diagnostics.clientCredentialsPresent,
|
|
40
|
-
fallbackTokenPresent: diagnostics.fallbackTokenPresent
|
|
40
|
+
fallbackTokenPresent: diagnostics.fallbackTokenPresent,
|
|
41
|
+
cacheTokenPresent: diagnostics.cacheTokenPresent,
|
|
42
|
+
cacheTokenExpired: diagnostics.cacheTokenExpired
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
if (diagnostics.runtimeTokenCached) {
|
|
@@ -114,7 +116,8 @@ async function logMusixmatchStatus(context) {
|
|
|
114
116
|
logger.warn('Musixmatch token missing', {
|
|
115
117
|
context,
|
|
116
118
|
provider: 'musixmatch',
|
|
117
|
-
details:
|
|
119
|
+
details:
|
|
120
|
+
'run npm run fetch:musixmatch-token to capture the cache token, or set MUSIXMATCH_FALLBACK_TOKEN as a fallback token for ephemeral deployments'
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
}
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
1
4
|
import axios from 'axios';
|
|
2
5
|
|
|
3
|
-
import { getEnvValue } from '../config.js';
|
|
6
|
+
import { getEnvValue, getProjectRoot } from '../config.js';
|
|
4
7
|
import { createLogger } from '../logger.js';
|
|
5
8
|
|
|
6
9
|
const GENIUS_TOKEN_ENDPOINT = 'https://api.genius.com/oauth/token';
|
|
7
10
|
const logger = createLogger('genius-token-manager');
|
|
8
11
|
|
|
12
|
+
// Token source terminology used throughout this module:
|
|
13
|
+
// • Auto-refresh — GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET env vars.
|
|
14
|
+
// The server calls the Genius OAuth client_credentials endpoint
|
|
15
|
+
// at runtime and keeps the token refreshed in memory automatically.
|
|
16
|
+
// This is the recommended approach for all deployments: no disk,
|
|
17
|
+
// no scripts, and no manual token copying needed.
|
|
18
|
+
// • Fallback token — GENIUS_ACCESS_TOKEN env var. A static bearer token set directly
|
|
19
|
+
// in the environment. Does not auto-refresh; redeploy when expired.
|
|
20
|
+
// Use this only when client_credentials are unavailable.
|
|
21
|
+
// • Cache token — on-disk .cache/genius-token.json written by the fetch script.
|
|
22
|
+
// Only reliable when a persistent, writable filesystem is available
|
|
23
|
+
// (i.e. local development). Ephemeral hosts (Render free tier, etc.)
|
|
24
|
+
// should use the auto-refresh or fallback token paths instead.
|
|
25
|
+
|
|
26
|
+
// Token cache path — must match the path used by scripts/fetch_genius_token.mjs.
|
|
27
|
+
const TOKEN_CACHE_PATH =
|
|
28
|
+
process.env.GENIUS_TOKEN_CACHE || path.join(getProjectRoot(), '.cache', 'genius-token.json');
|
|
29
|
+
|
|
9
30
|
let cachedToken = null;
|
|
10
31
|
let cachedExpiry = 0;
|
|
11
32
|
let lastAuthMode = 'unknown';
|
|
@@ -20,6 +41,21 @@ function tokenExpired() {
|
|
|
20
41
|
return now >= cachedExpiry - 60_000; // refresh one minute early
|
|
21
42
|
}
|
|
22
43
|
|
|
44
|
+
async function readCachedToken() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = await fs.readFile(TOKEN_CACHE_PATH, 'utf8');
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
const { access_token: accessToken, expires_at: expiresAt } = parsed ?? {};
|
|
49
|
+
if (!accessToken) return null;
|
|
50
|
+
// Skip if the cache token has already expired (with a 1-minute buffer).
|
|
51
|
+
if (expiresAt && Date.now() >= expiresAt - 60_000) return null;
|
|
52
|
+
return { accessToken, expiresAt };
|
|
53
|
+
} catch {
|
|
54
|
+
// Cache file absent or unreadable — not an error in remote environments.
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
async function fetchClientCredentialsToken() {
|
|
24
60
|
const clientId = getEnvValue('GENIUS_CLIENT_ID');
|
|
25
61
|
const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
|
|
@@ -55,22 +91,44 @@ async function fetchClientCredentialsToken() {
|
|
|
55
91
|
}
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the Genius token using the following priority order:
|
|
96
|
+
* 1. In-memory runtime cache (already resolved this session)
|
|
97
|
+
* 2. Auto-refresh via GENIUS_CLIENT_ID + GENIUS_CLIENT_SECRET (client_credentials)
|
|
98
|
+
* 3. GENIUS_ACCESS_TOKEN env var — fallback token
|
|
99
|
+
* 4. On-disk .cache/genius-token.json — cache token, local dev only
|
|
100
|
+
*/
|
|
58
101
|
export async function getGeniusToken({ forceRefresh = false } = {}) {
|
|
59
102
|
if (!forceRefresh && !tokenExpired()) {
|
|
60
103
|
return cachedToken;
|
|
61
104
|
}
|
|
105
|
+
|
|
106
|
+
// 2. Auto-refresh via client_credentials (ideal for all deployments)
|
|
62
107
|
const token = await fetchClientCredentialsToken();
|
|
63
108
|
if (token) {
|
|
64
109
|
return token;
|
|
65
110
|
}
|
|
111
|
+
|
|
112
|
+
// 3. Fallback token from env var (static, no auto-refresh)
|
|
66
113
|
const fallback = getFallbackToken();
|
|
67
114
|
if (fallback && fallback !== cachedToken) {
|
|
68
|
-
logger.warn('Using
|
|
115
|
+
logger.warn('Using Genius fallback token from GENIUS_ACCESS_TOKEN env var');
|
|
69
116
|
cachedToken = fallback;
|
|
70
117
|
cachedExpiry = Date.now() + 86_400_000; // 1 day placeholder
|
|
71
118
|
lastAuthMode = 'env_access_token';
|
|
72
119
|
return cachedToken;
|
|
73
120
|
}
|
|
121
|
+
|
|
122
|
+
// 4. Cache token from disk (local dev convenience only)
|
|
123
|
+
const cached = await readCachedToken();
|
|
124
|
+
if (cached) {
|
|
125
|
+
logger.info('Using Genius cache token from disk', { cachePath: TOKEN_CACHE_PATH });
|
|
126
|
+
cachedToken = cached.accessToken;
|
|
127
|
+
cachedExpiry = cached.expiresAt ?? Date.now() + 86_400_000;
|
|
128
|
+
lastAuthMode = 'cache';
|
|
129
|
+
return cachedToken;
|
|
130
|
+
}
|
|
131
|
+
|
|
74
132
|
return cachedToken;
|
|
75
133
|
}
|
|
76
134
|
|
|
@@ -103,17 +161,34 @@ export function describeGeniusAuthMode() {
|
|
|
103
161
|
return 'none';
|
|
104
162
|
}
|
|
105
163
|
|
|
106
|
-
export function getGeniusDiagnostics() {
|
|
164
|
+
export async function getGeniusDiagnostics() {
|
|
107
165
|
const clientId = getEnvValue('GENIUS_CLIENT_ID');
|
|
108
166
|
const clientSecret = getEnvValue('GENIUS_CLIENT_SECRET');
|
|
109
167
|
const fallback = getFallbackToken();
|
|
110
168
|
const ttlMs = Math.max(cachedExpiry - Date.now(), 0);
|
|
111
169
|
|
|
112
|
-
|
|
170
|
+
const diagnostics = {
|
|
113
171
|
clientCredentialsPresent: Boolean(clientId && clientSecret),
|
|
114
172
|
fallbackTokenPresent: Boolean(fallback),
|
|
115
173
|
runtimeTokenCached: Boolean(cachedToken),
|
|
116
174
|
runtimeTokenExpiresInMs: cachedToken ? ttlMs : 0,
|
|
117
|
-
lastAuthMode: describeGeniusAuthMode()
|
|
175
|
+
lastAuthMode: describeGeniusAuthMode(),
|
|
176
|
+
cachePath: TOKEN_CACHE_PATH,
|
|
177
|
+
cacheTokenPresent: false,
|
|
178
|
+
cacheTokenExpired: false,
|
|
179
|
+
cacheError: null
|
|
118
180
|
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const raw = await fs.readFile(TOKEN_CACHE_PATH, 'utf8');
|
|
184
|
+
const parsed = JSON.parse(raw);
|
|
185
|
+
diagnostics.cacheTokenPresent = Boolean(parsed?.access_token);
|
|
186
|
+
if (parsed?.expires_at) {
|
|
187
|
+
diagnostics.cacheTokenExpired = Date.now() >= parsed.expires_at - 60_000;
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
diagnostics.cacheError = error?.code === 'ENOENT' ? null : (error?.message ?? null);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return diagnostics;
|
|
119
194
|
}
|
|
@@ -11,7 +11,7 @@ const logger = createLogger('musixmatch-token-manager');
|
|
|
11
11
|
// Only reliable when a persistent, writable filesystem is available
|
|
12
12
|
// (i.e. local development). Ephemeral hosts (Render free tier, etc.)
|
|
13
13
|
// may not have a writable FS, so the cache token is unavailable there.
|
|
14
|
-
// • Fallback token — the token value supplied directly via
|
|
14
|
+
// • Fallback token — the token value supplied directly via MUSIXMATCH_FALLBACK_TOKEN or
|
|
15
15
|
// MUSIXMATCH_ALT_USER_TOKEN environment variables. This is the recommended
|
|
16
16
|
// approach for production and remote deployments where the filesystem
|
|
17
17
|
// cannot be relied upon for persistence.
|
|
@@ -60,7 +60,7 @@ async function writeCachedToken(token, desktopCookie) {
|
|
|
60
60
|
if (!dirOk) {
|
|
61
61
|
logger.warn(
|
|
62
62
|
'Musixmatch token cache directory unavailable (read-only or restricted filesystem). ' +
|
|
63
|
-
'Token was NOT persisted to disk. Set
|
|
63
|
+
'Token was NOT persisted to disk. Set MUSIXMATCH_FALLBACK_TOKEN as an environment variable ' +
|
|
64
64
|
'to ensure the token survives restarts in remote/ephemeral deployments.',
|
|
65
65
|
{ cachePath: TOKEN_CACHE_PATH }
|
|
66
66
|
);
|
|
@@ -78,7 +78,7 @@ async function writeCachedToken(token, desktopCookie) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Resolve the Musixmatch token using the following priority order:
|
|
80
80
|
* 1. In-memory runtime cache (already resolved this session)
|
|
81
|
-
* 2.
|
|
81
|
+
* 2. MUSIXMATCH_FALLBACK_TOKEN env var — fallback token, first-priority env source
|
|
82
82
|
* 3. MUSIXMATCH_ALT_USER_TOKEN env var — fallback token, second-priority env source
|
|
83
83
|
* 4. On-disk cache file — cache token, local dev only
|
|
84
84
|
*/
|
|
@@ -88,10 +88,10 @@ export async function getMusixmatchToken() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// Prioritize env vars — these survive restarts on ephemeral hosts
|
|
91
|
-
const userToken = getEnvValue('
|
|
91
|
+
const userToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
|
|
92
92
|
if (userToken) {
|
|
93
93
|
cachedToken = userToken;
|
|
94
|
-
lastLoadedFrom = 'env:
|
|
94
|
+
lastLoadedFrom = 'env:MUSIXMATCH_FALLBACK_TOKEN';
|
|
95
95
|
cachedDesktopCookie = null;
|
|
96
96
|
return cachedToken;
|
|
97
97
|
}
|
|
@@ -129,7 +129,7 @@ export function describeMusixmatchTokenSource() {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export async function getMusixmatchTokenDiagnostics() {
|
|
132
|
-
const userEnvToken = getEnvValue('
|
|
132
|
+
const userEnvToken = getEnvValue('MUSIXMATCH_FALLBACK_TOKEN');
|
|
133
133
|
const envToken = getEnvValue('MUSIXMATCH_ALT_USER_TOKEN');
|
|
134
134
|
|
|
135
135
|
const diagnostics = {
|
|
@@ -161,7 +161,7 @@ export async function getMusixmatchTokenDiagnostics() {
|
|
|
161
161
|
if (cachedToken) {
|
|
162
162
|
diagnostics.resolvedSource = lastLoadedFrom;
|
|
163
163
|
} else if (userEnvToken) {
|
|
164
|
-
diagnostics.resolvedSource = 'env:
|
|
164
|
+
diagnostics.resolvedSource = 'env:MUSIXMATCH_FALLBACK_TOKEN';
|
|
165
165
|
} else if (envToken) {
|
|
166
166
|
diagnostics.resolvedSource = 'env:MUSIXMATCH_ALT_USER_TOKEN';
|
|
167
167
|
} else if (diagnostics.cacheTokenPresent) {
|