@bzenky/spoti 0.2.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 +72 -4
- package/dist/app.d.ts +19 -1
- package/dist/app.js +310 -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/cli.js +53 -0
- package/dist/cli.js.map +1 -1
- package/dist/services/auth.service.d.ts +1 -1
- package/dist/services/auth.service.js +9 -3
- 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.js +22 -2
- package/dist/services/device.service.js.map +1 -1
- 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 +4 -1
- package/dist/services/player.service.js +21 -14
- 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/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 +1 -1
- package/dist/spotify/scopes.js +4 -0
- package/dist/spotify/scopes.js.map +1 -1
- package/dist/spotify/types.d.ts +110 -8
- package/dist/storage/config.d.ts +2 -1
- package/dist/storage/config.js +18 -1
- package/dist/storage/config.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/update-worker.d.ts +9 -0
- package/dist/update-worker.js +91 -0
- package/dist/update-worker.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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,32 +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';
|
|
4
5
|
import { DeviceService } from './services/device.service.js';
|
|
6
|
+
import { LibraryService } from './services/library.service.js';
|
|
5
7
|
import { PlayerService } from './services/player.service.js';
|
|
8
|
+
import { PlaylistService } from './services/playlist.service.js';
|
|
6
9
|
import { QueueService } from './services/queue.service.js';
|
|
10
|
+
import { RecentService } from './services/recent.service.js';
|
|
7
11
|
import { SearchService } from './services/search.service.js';
|
|
12
|
+
import { compareSemanticVersions, UPDATE_CHECK_INTERVAL_MS, UpdateService, } from './services/update.service.js';
|
|
8
13
|
import { FileConfigStore } from './storage/config.js';
|
|
14
|
+
import { FileUpdateCacheStore } from './storage/update-cache.js';
|
|
9
15
|
import { SpotifyClient } from './spotify/client.js';
|
|
10
16
|
import { consoleOutput } from './ui/output.js';
|
|
17
|
+
import { startUpdateWorker } from './update-worker.js';
|
|
11
18
|
import { AppError, toError } from './utils/errors.js';
|
|
19
|
+
import { VERSION } from './version.js';
|
|
12
20
|
const auth = new AuthService();
|
|
13
21
|
const spotify = new SpotifyClient(auth);
|
|
14
22
|
const device = new DeviceService(spotify);
|
|
23
|
+
const updateCache = new FileUpdateCacheStore();
|
|
24
|
+
const update = new UpdateService(VERSION, { cache: updateCache });
|
|
15
25
|
const program = createProgram({
|
|
16
26
|
auth,
|
|
27
|
+
catalog: new CatalogService(spotify),
|
|
17
28
|
device,
|
|
29
|
+
library: new LibraryService(spotify),
|
|
18
30
|
player: new PlayerService(spotify, device),
|
|
31
|
+
playlist: new PlaylistService(spotify),
|
|
19
32
|
queue: new QueueService(spotify),
|
|
33
|
+
recent: new RecentService(spotify),
|
|
20
34
|
search: new SearchService(spotify),
|
|
35
|
+
update,
|
|
21
36
|
config: new FileConfigStore(),
|
|
22
37
|
output: consoleOutput,
|
|
23
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
|
+
}
|
|
24
45
|
try {
|
|
25
46
|
await program.parseAsync(process.argv);
|
|
47
|
+
if (updateMessage && process.stderr.isTTY)
|
|
48
|
+
consoleOutput.error(updateMessage);
|
|
26
49
|
}
|
|
27
50
|
catch (error) {
|
|
28
51
|
const normalizedError = toError(error);
|
|
29
52
|
consoleOutput.error(normalizedError.message);
|
|
30
53
|
process.exitCode = error instanceof AppError ? error.exitCode : 1;
|
|
31
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
|
+
}
|
|
32
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,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,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;
|
|
@@ -4,6 +4,7 @@ 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);
|
|
@@ -93,7 +99,7 @@ export class AuthService {
|
|
|
93
99
|
return this.refreshPromise;
|
|
94
100
|
}
|
|
95
101
|
async performTokenRefresh(credentials) {
|
|
96
|
-
const config = this.configLoader();
|
|
102
|
+
const config = await this.configLoader();
|
|
97
103
|
let token;
|
|
98
104
|
try {
|
|
99
105
|
token = await this.spotifyAuthApi.refreshAccessToken({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../src/services/auth.service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,cAAc,GACf,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EACL,eAAe,EACf,iBAAiB,GAGlB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,cAAc,GAGf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,kBAAkB,GAAG,wCAAwC,CAAC;AAkBpE,MAAM,OAAO,WAAW;IACL,eAAe,CAAkB;IACjC,cAAc,CAAiB;IAC/B,YAAY,
|
|
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"}
|
|
@@ -6,7 +6,7 @@ export class DeviceService {
|
|
|
6
6
|
}
|
|
7
7
|
async getDevices() {
|
|
8
8
|
const response = await this.spotify.get('/me/player/devices');
|
|
9
|
-
return response.devices.map(mapDevice);
|
|
9
|
+
return response.devices.map(mapDevice).sort(compareDevices);
|
|
10
10
|
}
|
|
11
11
|
async getActiveDevice() {
|
|
12
12
|
const activeDevice = (await this.getDevices()).find((device) => device.isActive);
|
|
@@ -20,7 +20,19 @@ export class DeviceService {
|
|
|
20
20
|
async findDevice(nameOrId) {
|
|
21
21
|
const query = nameOrId.trim();
|
|
22
22
|
const normalizedQuery = query.toLocaleLowerCase();
|
|
23
|
-
const
|
|
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);
|
|
24
36
|
const idMatch = devices.find((device) => device.id?.toLocaleLowerCase() === normalizedQuery);
|
|
25
37
|
if (idMatch)
|
|
26
38
|
return idMatch;
|
|
@@ -46,6 +58,14 @@ export class DeviceService {
|
|
|
46
58
|
});
|
|
47
59
|
}
|
|
48
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
|
+
}
|
|
49
69
|
function mapDevice(device) {
|
|
50
70
|
return {
|
|
51
71
|
id: device.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device.service.js","sourceRoot":"","sources":["../../src/services/device.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA4BnE,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,OAAmB;QAAnB,YAAO,GAAP,OAAO,CAAY;IAAG,CAAC;IAEpD,KAAK,CAAC,UAAU;QACd,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAkB,oBAAoB,CAAC,CAAC;QAC/E,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;
|
|
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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"}
|
|
@@ -18,3 +18,48 @@ export interface CurrentPlayback {
|
|
|
18
18
|
progressMs: number;
|
|
19
19
|
deviceName?: string;
|
|
20
20
|
}
|
|
21
|
+
export interface Album {
|
|
22
|
+
id: string;
|
|
23
|
+
uri: string;
|
|
24
|
+
name: string;
|
|
25
|
+
artists: string[];
|
|
26
|
+
releaseDate?: string;
|
|
27
|
+
totalTracks: number;
|
|
28
|
+
imageUrl?: string;
|
|
29
|
+
externalUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface AlbumDetail extends Album {
|
|
32
|
+
tracks: Track[];
|
|
33
|
+
}
|
|
34
|
+
export interface Artist {
|
|
35
|
+
id: string;
|
|
36
|
+
uri: string;
|
|
37
|
+
name: string;
|
|
38
|
+
imageUrl?: string;
|
|
39
|
+
externalUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Playlist {
|
|
42
|
+
id: string;
|
|
43
|
+
uri: string;
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
ownerName: string;
|
|
47
|
+
isPublic: boolean | null;
|
|
48
|
+
totalTracks: number;
|
|
49
|
+
imageUrl?: string;
|
|
50
|
+
externalUrl?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PlaylistDetail extends Playlist {
|
|
53
|
+
tracks: Track[];
|
|
54
|
+
}
|
|
55
|
+
export interface SavedTrack {
|
|
56
|
+
addedAt: string;
|
|
57
|
+
track: Track;
|
|
58
|
+
}
|
|
59
|
+
export interface RecentlyPlayedTrack {
|
|
60
|
+
playedAt: string;
|
|
61
|
+
track: Track;
|
|
62
|
+
contextUri?: string;
|
|
63
|
+
}
|
|
64
|
+
export type RepeatMode = 'off' | 'track' | 'context';
|
|
65
|
+
export type SpotifyContextType = 'album' | 'artist' | 'playlist';
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { SpotifyApi } from '../spotify/client.js';
|
|
2
2
|
import type { DeviceService } from './device.service.js';
|
|
3
|
-
import type { CurrentPlayback } from './models.js';
|
|
3
|
+
import type { CurrentPlayback, RepeatMode } from './models.js';
|
|
4
4
|
export declare class PlayerService {
|
|
5
5
|
private readonly spotify;
|
|
6
6
|
private readonly deviceService?;
|
|
7
7
|
constructor(spotify: SpotifyApi, deviceService?: Pick<DeviceService, "getControllableDevices"> | undefined);
|
|
8
8
|
getCurrentPlayback(): Promise<CurrentPlayback | null>;
|
|
9
9
|
playTrack(uri: string): Promise<void>;
|
|
10
|
+
playContext(contextUri: string): Promise<void>;
|
|
11
|
+
setShuffle(state: boolean): Promise<void>;
|
|
12
|
+
setRepeat(state: RepeatMode): Promise<void>;
|
|
10
13
|
pause(): Promise<void>;
|
|
11
14
|
resume(): Promise<void>;
|
|
12
15
|
next(): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppError, NoActiveDeviceError } from '../utils/errors.js';
|
|
2
|
+
import { mapPlaybackItem } from './mappers.js';
|
|
2
3
|
export class PlayerService {
|
|
3
4
|
spotify;
|
|
4
5
|
deviceService;
|
|
@@ -10,9 +11,12 @@ export class PlayerService {
|
|
|
10
11
|
const playback = await this.spotify.get('/me/player');
|
|
11
12
|
if (!playback?.item)
|
|
12
13
|
return null;
|
|
14
|
+
const track = mapPlaybackItem(playback.item);
|
|
15
|
+
if (!track)
|
|
16
|
+
return null;
|
|
13
17
|
return {
|
|
14
18
|
isPlaying: playback.is_playing,
|
|
15
|
-
track
|
|
19
|
+
track,
|
|
16
20
|
progressMs: playback.progress_ms ?? 0,
|
|
17
21
|
deviceName: playback.device.name,
|
|
18
22
|
};
|
|
@@ -20,6 +24,21 @@ export class PlayerService {
|
|
|
20
24
|
async playTrack(uri) {
|
|
21
25
|
await this.putWithDeviceFallback('/me/player/play', { body: { uris: [uri] } });
|
|
22
26
|
}
|
|
27
|
+
async playContext(contextUri) {
|
|
28
|
+
await this.putWithDeviceFallback('/me/player/play', {
|
|
29
|
+
body: { context_uri: contextUri },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async setShuffle(state) {
|
|
33
|
+
await this.putWithDeviceFallback('/me/player/shuffle', {
|
|
34
|
+
query: { state },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async setRepeat(state) {
|
|
38
|
+
await this.putWithDeviceFallback('/me/player/repeat', {
|
|
39
|
+
query: { state },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
async pause() {
|
|
24
43
|
await this.spotify.put('/me/player/pause');
|
|
25
44
|
}
|
|
@@ -75,7 +94,7 @@ export class PlayerService {
|
|
|
75
94
|
const device = devices.length === 1 ? devices[0] : undefined;
|
|
76
95
|
if (!device?.id) {
|
|
77
96
|
if (devices.length > 1) {
|
|
78
|
-
throw new AppError('Multiple Spotify devices are available, but none is active.\n\nRun: spoti devices\nThen select one with: spoti device
|
|
97
|
+
throw new AppError('Multiple Spotify devices are available, but none is active.\n\nRun: spoti devices\nThen select one with: spoti device <number>');
|
|
79
98
|
}
|
|
80
99
|
throw new NoActiveDeviceError();
|
|
81
100
|
}
|
|
@@ -88,16 +107,4 @@ export class PlayerService {
|
|
|
88
107
|
function clampVolume(volume) {
|
|
89
108
|
return Math.min(100, Math.max(0, Math.round(volume)));
|
|
90
109
|
}
|
|
91
|
-
function mapTrack(track) {
|
|
92
|
-
const externalUrl = track.external_urls?.spotify;
|
|
93
|
-
return {
|
|
94
|
-
id: track.id,
|
|
95
|
-
uri: track.uri,
|
|
96
|
-
name: track.name,
|
|
97
|
-
artists: track.artists.map((artist) => artist.name),
|
|
98
|
-
album: track.album.name,
|
|
99
|
-
durationMs: track.duration_ms,
|
|
100
|
-
...(externalUrl ? { externalUrl } : {}),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
110
|
//# sourceMappingURL=player.service.js.map
|