@bzenky/spoti 0.1.0 → 0.3.0
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/README.md +144 -4
- package/dist/app.d.ts +27 -1
- package/dist/app.js +514 -16
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js +3 -3
- package/dist/auth/config.js.map +1 -1
- package/dist/auth/spotify-auth-client.d.ts +1 -0
- package/dist/auth/spotify-auth-client.js +4 -0
- package/dist/auth/spotify-auth-client.js.map +1 -1
- package/dist/cli.js +61 -1
- package/dist/cli.js.map +1 -1
- package/dist/services/auth.service.d.ts +1 -1
- package/dist/services/auth.service.js +17 -4
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/catalog.service.d.ts +9 -0
- package/dist/services/catalog.service.js +22 -0
- package/dist/services/catalog.service.js.map +1 -0
- package/dist/services/device.service.d.ts +20 -0
- package/dist/services/device.service.js +81 -0
- package/dist/services/device.service.js.map +1 -0
- package/dist/services/library.service.d.ts +13 -0
- package/dist/services/library.service.js +46 -0
- package/dist/services/library.service.js.map +1 -0
- package/dist/services/mappers.d.ts +10 -0
- package/dist/services/mappers.js +104 -0
- package/dist/services/mappers.js.map +1 -0
- package/dist/services/models.d.ts +45 -0
- package/dist/services/player.service.d.ts +12 -2
- package/dist/services/player.service.js +80 -15
- package/dist/services/player.service.js.map +1 -1
- package/dist/services/playlist.service.d.ts +9 -0
- package/dist/services/playlist.service.js +33 -0
- package/dist/services/playlist.service.js.map +1 -0
- package/dist/services/queue.service.d.ts +18 -0
- package/dist/services/queue.service.js +43 -0
- package/dist/services/queue.service.js.map +1 -0
- package/dist/services/recent.service.d.ts +7 -0
- package/dist/services/recent.service.js +24 -0
- package/dist/services/recent.service.js.map +1 -0
- package/dist/services/search.service.d.ts +5 -1
- package/dist/services/search.service.js +32 -17
- package/dist/services/search.service.js.map +1 -1
- package/dist/services/update.service.d.ts +57 -0
- package/dist/services/update.service.js +313 -0
- package/dist/services/update.service.js.map +1 -0
- package/dist/spotify/client.js +14 -1
- package/dist/spotify/client.js.map +1 -1
- package/dist/spotify/scopes.d.ts +2 -1
- package/dist/spotify/scopes.js +9 -1
- package/dist/spotify/scopes.js.map +1 -1
- package/dist/spotify/types.d.ts +112 -8
- package/dist/storage/config.d.ts +30 -0
- package/dist/storage/config.js +128 -0
- package/dist/storage/config.js.map +1 -0
- package/dist/storage/credentials.d.ts +1 -0
- package/dist/storage/credentials.js +1 -0
- package/dist/storage/credentials.js.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/update-cache.d.ts +29 -0
- package/dist/storage/update-cache.js +135 -0
- package/dist/storage/update-cache.js.map +1 -0
- package/dist/ui/output.d.ts +10 -1
- package/dist/ui/output.js +60 -0
- package/dist/ui/output.js.map +1 -1
- package/dist/ui/prompts.d.ts +12 -1
- package/dist/ui/prompts.js +73 -6
- package/dist/ui/prompts.js.map +1 -1
- package/dist/ui/watch.d.ts +7 -0
- package/dist/ui/watch.js +44 -0
- package/dist/ui/watch.js.map +1 -0
- package/dist/update-worker.d.ts +9 -0
- package/dist/update-worker.js +91 -0
- package/dist/update-worker.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
package/dist/auth/config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ConfigurationError } from '../utils/errors.js';
|
|
2
2
|
export const DEFAULT_REDIRECT_URI = 'http://127.0.0.1:43821/callback';
|
|
3
|
-
export function loadAuthConfig(environment = process.env) {
|
|
4
|
-
const clientId = environment.SPOTIFY_CLIENT_ID?.trim();
|
|
3
|
+
export function loadAuthConfig(environment = process.env, storedClientId = null) {
|
|
4
|
+
const clientId = environment.SPOTIFY_CLIENT_ID?.trim() || storedClientId?.trim();
|
|
5
5
|
if (!clientId) {
|
|
6
|
-
throw new ConfigurationError('
|
|
6
|
+
throw new ConfigurationError('A Spotify client ID is required.\n\nRun: spoti setup\nOr set SPOTIFY_CLIENT_ID in your environment.');
|
|
7
7
|
}
|
|
8
8
|
const redirectUri = environment.SPOTIFY_REDIRECT_URI?.trim() || DEFAULT_REDIRECT_URI;
|
|
9
9
|
validateRedirectUri(redirectUri);
|
package/dist/auth/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,CAAC,MAAM,oBAAoB,GAAG,iCAAiC,CAAC;AAOtE,MAAM,UAAU,cAAc,CAC5B,cAAiC,OAAO,CAAC,GAAG;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/auth/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,CAAC,MAAM,oBAAoB,GAAG,iCAAiC,CAAC;AAOtE,MAAM,UAAU,cAAc,CAC5B,cAAiC,OAAO,CAAC,GAAG,EAC5C,iBAAgC,IAAI;IAEpC,MAAM,QAAQ,GAAG,WAAW,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,cAAc,EAAE,IAAI,EAAE,CAAC;IACjF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,kBAAkB,CAC1B,qGAAqG,CACtG,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,WAAW,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,oBAAoB,CAAC;IACrF,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAEjC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,kBAAkB,CAAC,2CAA2C,CAAC,CAAC;IAC5E,CAAC;IAED,IACE,GAAG,CAAC,QAAQ,KAAK,OAAO;QACxB,GAAG,CAAC,QAAQ,KAAK,WAAW;QAC5B,CAAC,GAAG,CAAC,IAAI;QACT,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,QAAQ;QACZ,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,IAAI,EACR,CAAC;QACD,MAAM,IAAI,kBAAkB,CAC1B,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -8,6 +8,7 @@ const tokenResponseSchema = z.object({
|
|
|
8
8
|
access_token: z.string().min(1),
|
|
9
9
|
refresh_token: z.string().min(1).optional(),
|
|
10
10
|
expires_in: z.number().int().positive(),
|
|
11
|
+
scope: z.string().optional(),
|
|
11
12
|
});
|
|
12
13
|
const userProfileSchema = z.object({
|
|
13
14
|
id: z.string().min(1),
|
|
@@ -89,6 +90,9 @@ export class SpotifyAuthClient {
|
|
|
89
90
|
if (token.data.refresh_token !== undefined) {
|
|
90
91
|
result.refreshToken = token.data.refresh_token;
|
|
91
92
|
}
|
|
93
|
+
if (token.data.scope !== undefined) {
|
|
94
|
+
result.scopes = token.data.scope.split(' ').filter(Boolean);
|
|
95
|
+
}
|
|
92
96
|
return result;
|
|
93
97
|
}
|
|
94
98
|
async fetchWithTimeout(url, init) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotify-auth-client.js","sourceRoot":"","sources":["../../src/auth/spotify-auth-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,cAAc,GAAG,wCAAwC,CAAC;AAChE,MAAM,qBAAqB,GAAG,+BAA+B,CAAC;AAC9D,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAIjC,MAAM,KAAK,GAAU,CAAC,YAAY,EAAE,EAAE,CACpC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;AAE9D,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC3C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;
|
|
1
|
+
{"version":3,"file":"spotify-auth-client.js","sourceRoot":"","sources":["../../src/auth/spotify-auth-client.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProgram } from './app.js';
|
|
3
3
|
import { AuthService } from './services/auth.service.js';
|
|
4
|
+
import { CatalogService } from './services/catalog.service.js';
|
|
5
|
+
import { DeviceService } from './services/device.service.js';
|
|
6
|
+
import { LibraryService } from './services/library.service.js';
|
|
4
7
|
import { PlayerService } from './services/player.service.js';
|
|
8
|
+
import { PlaylistService } from './services/playlist.service.js';
|
|
9
|
+
import { QueueService } from './services/queue.service.js';
|
|
10
|
+
import { RecentService } from './services/recent.service.js';
|
|
5
11
|
import { SearchService } from './services/search.service.js';
|
|
12
|
+
import { compareSemanticVersions, UPDATE_CHECK_INTERVAL_MS, UpdateService, } from './services/update.service.js';
|
|
13
|
+
import { FileConfigStore } from './storage/config.js';
|
|
14
|
+
import { FileUpdateCacheStore } from './storage/update-cache.js';
|
|
6
15
|
import { SpotifyClient } from './spotify/client.js';
|
|
7
16
|
import { consoleOutput } from './ui/output.js';
|
|
17
|
+
import { startUpdateWorker } from './update-worker.js';
|
|
8
18
|
import { AppError, toError } from './utils/errors.js';
|
|
19
|
+
import { VERSION } from './version.js';
|
|
9
20
|
const auth = new AuthService();
|
|
10
21
|
const spotify = new SpotifyClient(auth);
|
|
22
|
+
const device = new DeviceService(spotify);
|
|
23
|
+
const updateCache = new FileUpdateCacheStore();
|
|
24
|
+
const update = new UpdateService(VERSION, { cache: updateCache });
|
|
11
25
|
const program = createProgram({
|
|
12
26
|
auth,
|
|
13
|
-
|
|
27
|
+
catalog: new CatalogService(spotify),
|
|
28
|
+
device,
|
|
29
|
+
library: new LibraryService(spotify),
|
|
30
|
+
player: new PlayerService(spotify, device),
|
|
31
|
+
playlist: new PlaylistService(spotify),
|
|
32
|
+
queue: new QueueService(spotify),
|
|
33
|
+
recent: new RecentService(spotify),
|
|
14
34
|
search: new SearchService(spotify),
|
|
35
|
+
update,
|
|
36
|
+
config: new FileConfigStore(),
|
|
15
37
|
output: consoleOutput,
|
|
16
38
|
});
|
|
39
|
+
const isUpdateCommand = process.argv[2] === 'update';
|
|
40
|
+
const cachedUpdate = isUpdateCommand ? null : await updateCache.read().catch(() => null);
|
|
41
|
+
const updateMessage = getCachedUpdateMessage(cachedUpdate, VERSION);
|
|
42
|
+
if (!isUpdateCommand && shouldRefreshUpdateCache(cachedUpdate, VERSION)) {
|
|
43
|
+
await startUpdateWorker(VERSION, updateCache.path);
|
|
44
|
+
}
|
|
17
45
|
try {
|
|
18
46
|
await program.parseAsync(process.argv);
|
|
47
|
+
if (updateMessage && process.stderr.isTTY)
|
|
48
|
+
consoleOutput.error(updateMessage);
|
|
19
49
|
}
|
|
20
50
|
catch (error) {
|
|
21
51
|
const normalizedError = toError(error);
|
|
22
52
|
consoleOutput.error(normalizedError.message);
|
|
23
53
|
process.exitCode = error instanceof AppError ? error.exitCode : 1;
|
|
24
54
|
}
|
|
55
|
+
function shouldRefreshUpdateCache(cache, currentVersion) {
|
|
56
|
+
if (!cache)
|
|
57
|
+
return true;
|
|
58
|
+
const age = Date.now() - cache.lastCheckedAt;
|
|
59
|
+
if (age < 0 || age >= UPDATE_CHECK_INTERVAL_MS)
|
|
60
|
+
return true;
|
|
61
|
+
return (cache.lastSuccessfulCheck !== undefined &&
|
|
62
|
+
cache.lastSuccessfulCheck.currentVersion !== currentVersion);
|
|
63
|
+
}
|
|
64
|
+
function getCachedUpdateMessage(cache, currentVersion) {
|
|
65
|
+
const successful = cache?.lastSuccessfulCheck;
|
|
66
|
+
if (!successful || successful.currentVersion !== currentVersion)
|
|
67
|
+
return null;
|
|
68
|
+
if (cache?.lastFailedCheckAt === cache?.lastCheckedAt)
|
|
69
|
+
return null;
|
|
70
|
+
const age = Date.now() - successful.checkedAt;
|
|
71
|
+
if (age < 0 || age >= UPDATE_CHECK_INTERVAL_MS)
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
if (compareSemanticVersions(successful.latestVersion, currentVersion) <= 0)
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
`Update available: ${currentVersion} → ${successful.latestVersion}`,
|
|
82
|
+
'Run: spoti update',
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
25
85
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ type AuthorizationCodeProvider = (authorizationUrl: string, redirectUri: string,
|
|
|
7
7
|
export interface AuthServiceDependencies {
|
|
8
8
|
credentialStore?: CredentialStore;
|
|
9
9
|
spotifyAuthApi?: SpotifyAuthApi;
|
|
10
|
-
loadConfig?: () => AuthConfig
|
|
10
|
+
loadConfig?: () => AuthConfig | Promise<AuthConfig>;
|
|
11
11
|
requestAuthorizationCode?: AuthorizationCodeProvider;
|
|
12
12
|
createPkcePair?: () => PkcePair;
|
|
13
13
|
createState?: () => string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { SPOTIFY_SCOPES } from '../spotify/scopes.js';
|
|
1
|
+
import { LEGACY_SPOTIFY_SCOPES, SPOTIFY_SCOPES, } from '../spotify/scopes.js';
|
|
2
2
|
import { generateOAuthState, generatePkcePair, } from '../auth/pkce.js';
|
|
3
3
|
import { loadAuthConfig } from '../auth/config.js';
|
|
4
4
|
import { requestAuthorizationCode } from '../auth/callback.js';
|
|
5
5
|
import { OAuthTokenError, SpotifyAuthClient, } from '../auth/spotify-auth-client.js';
|
|
6
6
|
import { FileCredentialStore, isTokenExpired, } from '../storage/credentials.js';
|
|
7
|
+
import { FileConfigStore } from '../storage/config.js';
|
|
7
8
|
import { AuthenticationRequiredError, ConfigurationError, } from '../utils/errors.js';
|
|
8
9
|
const AUTHORIZE_ENDPOINT = 'https://accounts.spotify.com/authorize';
|
|
9
10
|
export class AuthService {
|
|
@@ -18,7 +19,12 @@ export class AuthService {
|
|
|
18
19
|
constructor(dependencies = {}) {
|
|
19
20
|
this.credentialStore = dependencies.credentialStore ?? new FileCredentialStore();
|
|
20
21
|
this.spotifyAuthApi = dependencies.spotifyAuthApi ?? new SpotifyAuthClient();
|
|
21
|
-
this.configLoader =
|
|
22
|
+
this.configLoader =
|
|
23
|
+
dependencies.loadConfig ??
|
|
24
|
+
(async () => {
|
|
25
|
+
const config = await new FileConfigStore().read();
|
|
26
|
+
return loadAuthConfig(process.env, config.spotifyClientId);
|
|
27
|
+
});
|
|
22
28
|
this.authorizationCodeProvider =
|
|
23
29
|
dependencies.requestAuthorizationCode ?? requestAuthorizationCode;
|
|
24
30
|
this.pkceFactory = dependencies.createPkcePair ?? generatePkcePair;
|
|
@@ -26,7 +32,7 @@ export class AuthService {
|
|
|
26
32
|
this.clock = dependencies.now ?? Date.now;
|
|
27
33
|
}
|
|
28
34
|
async login() {
|
|
29
|
-
const config = this.configLoader();
|
|
35
|
+
const config = await this.configLoader();
|
|
30
36
|
const pkce = this.pkceFactory();
|
|
31
37
|
const state = this.stateFactory();
|
|
32
38
|
const authorizationUrl = createAuthorizationUrl(config, pkce.challenge, state);
|
|
@@ -44,6 +50,7 @@ export class AuthService {
|
|
|
44
50
|
accessToken: token.accessToken,
|
|
45
51
|
refreshToken: token.refreshToken,
|
|
46
52
|
expiresAt: this.clock() + token.expiresIn * 1_000,
|
|
53
|
+
scopes: token.scopes ?? [...SPOTIFY_SCOPES],
|
|
47
54
|
};
|
|
48
55
|
const user = await this.spotifyAuthApi.getCurrentUser(credentials.accessToken);
|
|
49
56
|
await this.credentialStore.write(credentials);
|
|
@@ -56,6 +63,11 @@ export class AuthService {
|
|
|
56
63
|
const credentials = await this.credentialStore.read();
|
|
57
64
|
if (!credentials)
|
|
58
65
|
throw new AuthenticationRequiredError();
|
|
66
|
+
const grantedScopes = credentials.scopes ?? [...LEGACY_SPOTIFY_SCOPES];
|
|
67
|
+
const missingScopes = SPOTIFY_SCOPES.filter((scope) => !grantedScopes.includes(scope));
|
|
68
|
+
if (missingScopes.length > 0) {
|
|
69
|
+
throw new AuthenticationRequiredError(`spoti needs new Spotify permissions (${missingScopes.join(', ')}).\n\nRun: spoti login`);
|
|
70
|
+
}
|
|
59
71
|
if (!isTokenExpired(credentials, this.clock()))
|
|
60
72
|
return credentials.accessToken;
|
|
61
73
|
return this.startTokenRefresh(credentials);
|
|
@@ -87,7 +99,7 @@ export class AuthService {
|
|
|
87
99
|
return this.refreshPromise;
|
|
88
100
|
}
|
|
89
101
|
async performTokenRefresh(credentials) {
|
|
90
|
-
const config = this.configLoader();
|
|
102
|
+
const config = await this.configLoader();
|
|
91
103
|
let token;
|
|
92
104
|
try {
|
|
93
105
|
token = await this.spotifyAuthApi.refreshAccessToken({
|
|
@@ -106,6 +118,7 @@ export class AuthService {
|
|
|
106
118
|
accessToken: token.accessToken,
|
|
107
119
|
refreshToken: token.refreshToken ?? credentials.refreshToken,
|
|
108
120
|
expiresAt: this.clock() + token.expiresIn * 1_000,
|
|
121
|
+
scopes: token.scopes ?? credentials.scopes ?? [...LEGACY_SPOTIFY_SCOPES],
|
|
109
122
|
};
|
|
110
123
|
await this.credentialStore.write(updatedCredentials);
|
|
111
124
|
return updatedCredentials.accessToken;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../src/services/auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../src/services/auth.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SpotifyApi } from '../spotify/client.js';
|
|
2
|
+
import type { Album, AlbumDetail, Artist } from './models.js';
|
|
3
|
+
export declare class CatalogService {
|
|
4
|
+
private readonly spotify;
|
|
5
|
+
constructor(spotify: SpotifyApi);
|
|
6
|
+
getAlbum(id: string): Promise<AlbumDetail>;
|
|
7
|
+
getArtist(id: string): Promise<Artist>;
|
|
8
|
+
getArtistAlbums(id: string, limit?: number): Promise<Album[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mapAlbum, mapAlbumDetail, mapArtist, normalizeLimit } from './mappers.js';
|
|
2
|
+
export class CatalogService {
|
|
3
|
+
spotify;
|
|
4
|
+
constructor(spotify) {
|
|
5
|
+
this.spotify = spotify;
|
|
6
|
+
}
|
|
7
|
+
async getAlbum(id) {
|
|
8
|
+
const album = await this.spotify.get(`/albums/${encodeURIComponent(id)}`);
|
|
9
|
+
return mapAlbumDetail(album);
|
|
10
|
+
}
|
|
11
|
+
async getArtist(id) {
|
|
12
|
+
const artist = await this.spotify.get(`/artists/${encodeURIComponent(id)}`);
|
|
13
|
+
return mapArtist(artist);
|
|
14
|
+
}
|
|
15
|
+
async getArtistAlbums(id, limit = 10) {
|
|
16
|
+
const response = await this.spotify.get(`/artists/${encodeURIComponent(id)}/albums`, { query: { limit: normalizeLimit(limit, 10, 10) } });
|
|
17
|
+
return response.items
|
|
18
|
+
.map(mapAlbum)
|
|
19
|
+
.filter((album) => album !== null);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=catalog.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.service.js","sourceRoot":"","sources":["../../src/services/catalog.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnF,MAAM,OAAO,cAAc;IACI;IAA7B,YAA6B,OAAmB;QAAnB,YAAO,GAAP,OAAO,CAAY;IAAG,CAAC;IAEpD,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAe,WAAW,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACxF,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CACnC,YAAY,kBAAkB,CAAC,EAAE,CAAC,EAAE,CACrC,CAAC;QACF,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,EAAU,EAAE,KAAK,GAAG,EAAE;QAC1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CACrC,YAAY,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAC3C,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CACpD,CAAC;QACF,OAAO,QAAQ,CAAC,KAAK;aAClB,GAAG,CAAC,QAAQ,CAAC;aACb,MAAM,CAAC,CAAC,KAAK,EAAkB,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;IACvD,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SpotifyApi } from '../spotify/client.js';
|
|
2
|
+
export interface Device {
|
|
3
|
+
id: string | null;
|
|
4
|
+
isActive: boolean;
|
|
5
|
+
isPrivateSession: boolean;
|
|
6
|
+
isRestricted: boolean;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
volumePercent: number | null;
|
|
10
|
+
supportsVolume: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class DeviceService {
|
|
13
|
+
private readonly spotify;
|
|
14
|
+
constructor(spotify: SpotifyApi);
|
|
15
|
+
getDevices(): Promise<Device[]>;
|
|
16
|
+
getActiveDevice(): Promise<Device>;
|
|
17
|
+
getControllableDevices(): Promise<Device[]>;
|
|
18
|
+
findDevice(nameOrId: string): Promise<Device>;
|
|
19
|
+
transferPlayback(deviceId: string, play?: boolean): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AppError, NoActiveDeviceError } from '../utils/errors.js';
|
|
2
|
+
export class DeviceService {
|
|
3
|
+
spotify;
|
|
4
|
+
constructor(spotify) {
|
|
5
|
+
this.spotify = spotify;
|
|
6
|
+
}
|
|
7
|
+
async getDevices() {
|
|
8
|
+
const response = await this.spotify.get('/me/player/devices');
|
|
9
|
+
return response.devices.map(mapDevice).sort(compareDevices);
|
|
10
|
+
}
|
|
11
|
+
async getActiveDevice() {
|
|
12
|
+
const activeDevice = (await this.getDevices()).find((device) => device.isActive);
|
|
13
|
+
if (!activeDevice)
|
|
14
|
+
throw new NoActiveDeviceError();
|
|
15
|
+
return activeDevice;
|
|
16
|
+
}
|
|
17
|
+
async getControllableDevices() {
|
|
18
|
+
return (await this.getDevices()).filter((device) => device.id !== null && !device.isRestricted);
|
|
19
|
+
}
|
|
20
|
+
async findDevice(nameOrId) {
|
|
21
|
+
const query = nameOrId.trim();
|
|
22
|
+
const normalizedQuery = query.toLocaleLowerCase();
|
|
23
|
+
const availableDevices = await this.getDevices();
|
|
24
|
+
if (/^\d+$/.test(query)) {
|
|
25
|
+
const index = Number(query) - 1;
|
|
26
|
+
const selected = Number.isSafeInteger(index) ? availableDevices[index] : undefined;
|
|
27
|
+
if (!selected) {
|
|
28
|
+
throw new AppError(`Device number ${query} is out of range. Run: spoti devices`);
|
|
29
|
+
}
|
|
30
|
+
if (!selected.id || selected.isRestricted) {
|
|
31
|
+
throw new AppError(`Device ${query} ("${selected.name}") cannot be controlled through Spotify Connect.`);
|
|
32
|
+
}
|
|
33
|
+
return selected;
|
|
34
|
+
}
|
|
35
|
+
const devices = availableDevices.filter((device) => device.id !== null && !device.isRestricted);
|
|
36
|
+
const idMatch = devices.find((device) => device.id?.toLocaleLowerCase() === normalizedQuery);
|
|
37
|
+
if (idMatch)
|
|
38
|
+
return idMatch;
|
|
39
|
+
const nameMatches = devices.filter((device) => device.name.toLocaleLowerCase() === normalizedQuery);
|
|
40
|
+
if (nameMatches.length === 1)
|
|
41
|
+
return nameMatches[0];
|
|
42
|
+
if (nameMatches.length > 1) {
|
|
43
|
+
const deviceIds = nameMatches.map((device) => device.id).join(', ');
|
|
44
|
+
throw new AppError(`Multiple controllable Spotify devices are named "${query}". Use a device ID instead: ${deviceIds}.`);
|
|
45
|
+
}
|
|
46
|
+
throw new AppError(`No controllable Spotify device matches "${query}". Check the device name or ID and ensure Spotify is open on that device.`);
|
|
47
|
+
}
|
|
48
|
+
async transferPlayback(deviceId, play) {
|
|
49
|
+
const normalizedDeviceId = deviceId.trim();
|
|
50
|
+
if (!normalizedDeviceId) {
|
|
51
|
+
throw new AppError('A Spotify device ID is required to transfer playback.');
|
|
52
|
+
}
|
|
53
|
+
await this.spotify.put('/me/player', {
|
|
54
|
+
body: {
|
|
55
|
+
device_ids: [normalizedDeviceId],
|
|
56
|
+
...(play === undefined ? {} : { play }),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function compareDevices(left, right) {
|
|
62
|
+
const nameComparison = left.name.localeCompare(right.name, undefined, {
|
|
63
|
+
sensitivity: 'base',
|
|
64
|
+
});
|
|
65
|
+
if (nameComparison !== 0)
|
|
66
|
+
return nameComparison;
|
|
67
|
+
return (left.id ?? '').localeCompare(right.id ?? '');
|
|
68
|
+
}
|
|
69
|
+
function mapDevice(device) {
|
|
70
|
+
return {
|
|
71
|
+
id: device.id,
|
|
72
|
+
isActive: device.is_active,
|
|
73
|
+
isPrivateSession: device.is_private_session,
|
|
74
|
+
isRestricted: device.is_restricted,
|
|
75
|
+
name: device.name,
|
|
76
|
+
type: device.type,
|
|
77
|
+
volumePercent: device.volume_percent,
|
|
78
|
+
supportsVolume: device.supports_volume,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=device.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.service.js","sourceRoot":"","sources":["../../src/services/device.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SpotifyApi } from '../spotify/client.js';
|
|
2
|
+
import type { SavedTrack, Track } from './models.js';
|
|
3
|
+
export declare class LibraryService {
|
|
4
|
+
private readonly spotify;
|
|
5
|
+
constructor(spotify: SpotifyApi);
|
|
6
|
+
getLikedTracks(limit?: number): Promise<SavedTrack[]>;
|
|
7
|
+
likeTrack(uri: string): Promise<void>;
|
|
8
|
+
unlikeTrack(uri: string): Promise<void>;
|
|
9
|
+
likeCurrentTrack(): Promise<Track | null>;
|
|
10
|
+
unlikeCurrentTrack(): Promise<Track | null>;
|
|
11
|
+
private changeCurrentTrackSavedState;
|
|
12
|
+
private changeSavedState;
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mapPlaybackItem, mapTrack, normalizeLimit } from './mappers.js';
|
|
2
|
+
const DEFAULT_LIBRARY_LIMIT = 20;
|
|
3
|
+
export class LibraryService {
|
|
4
|
+
spotify;
|
|
5
|
+
constructor(spotify) {
|
|
6
|
+
this.spotify = spotify;
|
|
7
|
+
}
|
|
8
|
+
async getLikedTracks(limit = DEFAULT_LIBRARY_LIMIT) {
|
|
9
|
+
const response = await this.spotify.get('/me/tracks', {
|
|
10
|
+
query: { limit: normalizeLimit(limit) },
|
|
11
|
+
});
|
|
12
|
+
return response.items.flatMap(({ added_at: addedAt, track }) => {
|
|
13
|
+
const mapped = mapTrack(track);
|
|
14
|
+
return mapped ? [{ addedAt, track: mapped }] : [];
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async likeTrack(uri) {
|
|
18
|
+
await this.changeSavedState('put', uri);
|
|
19
|
+
}
|
|
20
|
+
async unlikeTrack(uri) {
|
|
21
|
+
await this.changeSavedState('delete', uri);
|
|
22
|
+
}
|
|
23
|
+
async likeCurrentTrack() {
|
|
24
|
+
return this.changeCurrentTrackSavedState('put');
|
|
25
|
+
}
|
|
26
|
+
async unlikeCurrentTrack() {
|
|
27
|
+
return this.changeCurrentTrackSavedState('delete');
|
|
28
|
+
}
|
|
29
|
+
async changeCurrentTrackSavedState(method) {
|
|
30
|
+
const playback = await this.spotify.get('/me/player');
|
|
31
|
+
const track = mapPlaybackItem(playback?.item ?? null);
|
|
32
|
+
if (!track)
|
|
33
|
+
return null;
|
|
34
|
+
await this.changeSavedState(method, track.uri);
|
|
35
|
+
return track;
|
|
36
|
+
}
|
|
37
|
+
async changeSavedState(method, uri) {
|
|
38
|
+
const normalizedUri = uri.trim();
|
|
39
|
+
if (!normalizedUri.startsWith('spotify:track:'))
|
|
40
|
+
return;
|
|
41
|
+
await this.spotify[method]('/me/library', {
|
|
42
|
+
query: { uris: normalizedUri },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=library.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library.service.js","sourceRoot":"","sources":["../../src/services/library.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGzE,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC,MAAM,OAAO,cAAc;IACI;IAA7B,YAA6B,OAAmB;QAAnB,YAAO,GAAP,OAAO,CAAY;IAAG,CAAC;IAEpD,KAAK,CAAC,cAAc,CAAC,KAAK,GAAG,qBAAqB;QAChD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAmC,YAAY,EAAE;YACtF,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE;SACxC,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE;YAC7D,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC/B,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,MAAM,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,IAAI,CAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,4BAA4B,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAEO,KAAK,CAAC,4BAA4B,CACxC,MAAwB;QAExB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAmC,YAAY,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,MAAwB,EAAE,GAAW;QAClE,MAAM,aAAa,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,gBAAgB,CAAC;YAAE,OAAO;QACxD,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAO,aAAa,EAAE;YAC9C,KAAK,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SpotifyAlbum, SpotifyFullArtist, SpotifyPlaybackItem, SpotifyPlaylist, SpotifySimplifiedAlbum, SpotifySimplifiedPlaylist, SpotifySimplifiedTrack, SpotifyTrack } from '../spotify/types.js';
|
|
2
|
+
import type { Album, AlbumDetail, Artist, Playlist, Track } from './models.js';
|
|
3
|
+
export declare function mapTrack(track: SpotifyTrack): Track | null;
|
|
4
|
+
export declare function mapSimplifiedTrack(track: SpotifySimplifiedTrack, albumName: string): Track | null;
|
|
5
|
+
export declare function mapPlaybackItem(item: SpotifyPlaybackItem | null): Track | null;
|
|
6
|
+
export declare function mapAlbum(album: SpotifySimplifiedAlbum): Album | null;
|
|
7
|
+
export declare function mapAlbumDetail(album: SpotifyAlbum): AlbumDetail;
|
|
8
|
+
export declare function mapArtist(artist: SpotifyFullArtist): Artist;
|
|
9
|
+
export declare function mapPlaylist(playlist: SpotifySimplifiedPlaylist | SpotifyPlaylist): Playlist;
|
|
10
|
+
export declare function normalizeLimit(limit: number, defaultLimit?: number, maximum?: number): number;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function mapTrack(track) {
|
|
2
|
+
if (!isSpotifyTrack(track) || track.is_local || track.is_playable === false)
|
|
3
|
+
return null;
|
|
4
|
+
return mapTrackFields(track, track.album.name);
|
|
5
|
+
}
|
|
6
|
+
export function mapSimplifiedTrack(track, albumName) {
|
|
7
|
+
if (!isSpotifyTrack(track) || track.is_local || track.is_playable === false)
|
|
8
|
+
return null;
|
|
9
|
+
return mapTrackFields(track, albumName);
|
|
10
|
+
}
|
|
11
|
+
export function mapPlaybackItem(item) {
|
|
12
|
+
return isFullSpotifyTrack(item) ? mapTrack(item) : null;
|
|
13
|
+
}
|
|
14
|
+
export function mapAlbum(album) {
|
|
15
|
+
if (!album.id || !album.uri)
|
|
16
|
+
return null;
|
|
17
|
+
const imageUrl = album.images[0]?.url;
|
|
18
|
+
const externalUrl = album.external_urls?.spotify;
|
|
19
|
+
return {
|
|
20
|
+
id: album.id,
|
|
21
|
+
uri: album.uri,
|
|
22
|
+
name: album.name,
|
|
23
|
+
artists: (album.artists ?? []).map((artist) => artist.name),
|
|
24
|
+
totalTracks: album.total_tracks ?? 0,
|
|
25
|
+
...(album.release_date ? { releaseDate: album.release_date } : {}),
|
|
26
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
27
|
+
...(externalUrl ? { externalUrl } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function mapAlbumDetail(album) {
|
|
31
|
+
const mapped = mapAlbum(album);
|
|
32
|
+
const base = mapped ?? {
|
|
33
|
+
id: album.id,
|
|
34
|
+
uri: album.uri,
|
|
35
|
+
name: album.name,
|
|
36
|
+
artists: album.artists.map((artist) => artist.name),
|
|
37
|
+
totalTracks: album.total_tracks ?? album.tracks.total,
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
tracks: album.tracks.items
|
|
42
|
+
.map((track) => mapSimplifiedTrack(track, album.name))
|
|
43
|
+
.filter((track) => track !== null),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function mapArtist(artist) {
|
|
47
|
+
const imageUrl = artist.images[0]?.url;
|
|
48
|
+
const externalUrl = artist.external_urls?.spotify;
|
|
49
|
+
return {
|
|
50
|
+
id: artist.id,
|
|
51
|
+
uri: artist.uri,
|
|
52
|
+
name: artist.name,
|
|
53
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
54
|
+
...(externalUrl ? { externalUrl } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function mapPlaylist(playlist) {
|
|
58
|
+
const imageUrl = playlist.images?.[0]?.url;
|
|
59
|
+
const externalUrl = playlist.external_urls?.spotify;
|
|
60
|
+
return {
|
|
61
|
+
id: playlist.id,
|
|
62
|
+
uri: playlist.uri,
|
|
63
|
+
name: playlist.name,
|
|
64
|
+
description: playlist.description ?? '',
|
|
65
|
+
ownerName: playlist.owner.display_name ?? playlist.owner.id,
|
|
66
|
+
isPublic: playlist.public,
|
|
67
|
+
totalTracks: playlist.items.total,
|
|
68
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
69
|
+
...(externalUrl ? { externalUrl } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function normalizeLimit(limit, defaultLimit = 20, maximum = 50) {
|
|
73
|
+
if (!Number.isFinite(limit))
|
|
74
|
+
return defaultLimit;
|
|
75
|
+
return Math.min(maximum, Math.max(1, Math.round(limit)));
|
|
76
|
+
}
|
|
77
|
+
function mapTrackFields(track, albumName) {
|
|
78
|
+
const externalUrl = track.external_urls?.spotify;
|
|
79
|
+
return {
|
|
80
|
+
id: track.id,
|
|
81
|
+
uri: track.uri,
|
|
82
|
+
name: track.name,
|
|
83
|
+
artists: track.artists.map((artist) => artist.name),
|
|
84
|
+
album: albumName,
|
|
85
|
+
durationMs: track.duration_ms,
|
|
86
|
+
...(externalUrl ? { externalUrl } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function isSpotifyTrack(item) {
|
|
90
|
+
return (typeof item.id === 'string' &&
|
|
91
|
+
item.id.length > 0 &&
|
|
92
|
+
typeof item.uri === 'string' &&
|
|
93
|
+
item.uri.startsWith('spotify:track:') &&
|
|
94
|
+
typeof item.name === 'string' &&
|
|
95
|
+
typeof item.duration_ms === 'number' &&
|
|
96
|
+
Array.isArray(item.artists));
|
|
97
|
+
}
|
|
98
|
+
function isFullSpotifyTrack(item) {
|
|
99
|
+
if (!item || item.type === 'episode' || !('album' in item))
|
|
100
|
+
return false;
|
|
101
|
+
const candidate = item;
|
|
102
|
+
return Boolean(candidate.album && typeof candidate.album.name === 'string');
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=mappers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mappers.js","sourceRoot":"","sources":["../../src/services/mappers.ts"],"names":[],"mappings":"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"}
|