@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.
Files changed (80) hide show
  1. package/README.md +144 -4
  2. package/dist/app.d.ts +27 -1
  3. package/dist/app.js +514 -16
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth/config.d.ts +1 -1
  6. package/dist/auth/config.js +3 -3
  7. package/dist/auth/config.js.map +1 -1
  8. package/dist/auth/spotify-auth-client.d.ts +1 -0
  9. package/dist/auth/spotify-auth-client.js +4 -0
  10. package/dist/auth/spotify-auth-client.js.map +1 -1
  11. package/dist/cli.js +61 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/services/auth.service.d.ts +1 -1
  14. package/dist/services/auth.service.js +17 -4
  15. package/dist/services/auth.service.js.map +1 -1
  16. package/dist/services/catalog.service.d.ts +9 -0
  17. package/dist/services/catalog.service.js +22 -0
  18. package/dist/services/catalog.service.js.map +1 -0
  19. package/dist/services/device.service.d.ts +20 -0
  20. package/dist/services/device.service.js +81 -0
  21. package/dist/services/device.service.js.map +1 -0
  22. package/dist/services/library.service.d.ts +13 -0
  23. package/dist/services/library.service.js +46 -0
  24. package/dist/services/library.service.js.map +1 -0
  25. package/dist/services/mappers.d.ts +10 -0
  26. package/dist/services/mappers.js +104 -0
  27. package/dist/services/mappers.js.map +1 -0
  28. package/dist/services/models.d.ts +45 -0
  29. package/dist/services/player.service.d.ts +12 -2
  30. package/dist/services/player.service.js +80 -15
  31. package/dist/services/player.service.js.map +1 -1
  32. package/dist/services/playlist.service.d.ts +9 -0
  33. package/dist/services/playlist.service.js +33 -0
  34. package/dist/services/playlist.service.js.map +1 -0
  35. package/dist/services/queue.service.d.ts +18 -0
  36. package/dist/services/queue.service.js +43 -0
  37. package/dist/services/queue.service.js.map +1 -0
  38. package/dist/services/recent.service.d.ts +7 -0
  39. package/dist/services/recent.service.js +24 -0
  40. package/dist/services/recent.service.js.map +1 -0
  41. package/dist/services/search.service.d.ts +5 -1
  42. package/dist/services/search.service.js +32 -17
  43. package/dist/services/search.service.js.map +1 -1
  44. package/dist/services/update.service.d.ts +57 -0
  45. package/dist/services/update.service.js +313 -0
  46. package/dist/services/update.service.js.map +1 -0
  47. package/dist/spotify/client.js +14 -1
  48. package/dist/spotify/client.js.map +1 -1
  49. package/dist/spotify/scopes.d.ts +2 -1
  50. package/dist/spotify/scopes.js +9 -1
  51. package/dist/spotify/scopes.js.map +1 -1
  52. package/dist/spotify/types.d.ts +112 -8
  53. package/dist/storage/config.d.ts +30 -0
  54. package/dist/storage/config.js +128 -0
  55. package/dist/storage/config.js.map +1 -0
  56. package/dist/storage/credentials.d.ts +1 -0
  57. package/dist/storage/credentials.js +1 -0
  58. package/dist/storage/credentials.js.map +1 -1
  59. package/dist/storage/index.d.ts +2 -0
  60. package/dist/storage/index.js +1 -0
  61. package/dist/storage/index.js.map +1 -1
  62. package/dist/storage/update-cache.d.ts +29 -0
  63. package/dist/storage/update-cache.js +135 -0
  64. package/dist/storage/update-cache.js.map +1 -0
  65. package/dist/ui/output.d.ts +10 -1
  66. package/dist/ui/output.js +60 -0
  67. package/dist/ui/output.js.map +1 -1
  68. package/dist/ui/prompts.d.ts +12 -1
  69. package/dist/ui/prompts.js +73 -6
  70. package/dist/ui/prompts.js.map +1 -1
  71. package/dist/ui/watch.d.ts +7 -0
  72. package/dist/ui/watch.js +44 -0
  73. package/dist/ui/watch.js.map +1 -0
  74. package/dist/update-worker.d.ts +9 -0
  75. package/dist/update-worker.js +91 -0
  76. package/dist/update-worker.js.map +1 -0
  77. package/dist/version.d.ts +2 -0
  78. package/dist/version.js +11 -0
  79. package/dist/version.js.map +1 -0
  80. package/package.json +1 -1
@@ -0,0 +1,128 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { ConfigurationError, toError } from '../utils/errors.js';
6
+ import { getConfigDirectory } from './credentials.js';
7
+ const spotifyClientIdSchema = z
8
+ .string()
9
+ .trim()
10
+ .min(1)
11
+ .max(256)
12
+ .regex(/^[A-Za-z0-9]+$/);
13
+ const configSchema = z.strictObject({
14
+ spotifyClientId: spotifyClientIdSchema.nullable().default(null),
15
+ watchAfterPlay: z.boolean().default(false),
16
+ refreshIntervalMs: z.number().int().min(1_000).max(30_000).default(1_000),
17
+ });
18
+ export const CONFIG_KEYS = [
19
+ 'spotifyClientId',
20
+ 'watchAfterPlay',
21
+ 'refreshIntervalMs',
22
+ ];
23
+ export const DEFAULT_CONFIG = Object.freeze(configSchema.parse({}));
24
+ export class FileConfigStore {
25
+ path;
26
+ directory;
27
+ constructor(options = {}) {
28
+ this.directory = options.directory ?? getConfigDirectory();
29
+ this.path = join(this.directory, 'config.json');
30
+ }
31
+ async read() {
32
+ let contents;
33
+ try {
34
+ contents = await readFile(this.path, 'utf8');
35
+ }
36
+ catch (error) {
37
+ if (isFileNotFound(error))
38
+ return { ...DEFAULT_CONFIG };
39
+ throw new ConfigurationError(`Unable to read application configuration: ${toError(error).message}`);
40
+ }
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(contents);
44
+ }
45
+ catch {
46
+ throw new ConfigurationError('Application configuration is not valid JSON.');
47
+ }
48
+ const config = configSchema.safeParse(parsed);
49
+ if (!config.success) {
50
+ throw new ConfigurationError('Application configuration has an invalid format.');
51
+ }
52
+ return config.data;
53
+ }
54
+ async write(config) {
55
+ const validated = configSchema.safeParse(config);
56
+ if (!validated.success) {
57
+ throw new ConfigurationError('Cannot store invalid application configuration.');
58
+ }
59
+ try {
60
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
61
+ await chmod(this.directory, 0o700);
62
+ }
63
+ catch (error) {
64
+ throw new ConfigurationError(`Unable to prepare the application configuration directory: ${toError(error).message}`);
65
+ }
66
+ const temporaryPath = join(this.directory, `.config-${randomUUID()}.tmp`);
67
+ try {
68
+ await writeFile(temporaryPath, `${JSON.stringify(validated.data, null, 2)}\n`, {
69
+ encoding: 'utf8',
70
+ flag: 'wx',
71
+ mode: 0o600,
72
+ });
73
+ await chmod(temporaryPath, 0o600);
74
+ await rename(temporaryPath, this.path);
75
+ await chmod(this.path, 0o600);
76
+ }
77
+ catch (error) {
78
+ await unlink(temporaryPath).catch(() => undefined);
79
+ throw new ConfigurationError(`Unable to store application configuration: ${toError(error).message}`);
80
+ }
81
+ }
82
+ async set(key, value) {
83
+ const current = await this.read();
84
+ await this.write({ ...current, [key]: value });
85
+ }
86
+ async reset() {
87
+ try {
88
+ await unlink(this.path);
89
+ }
90
+ catch (error) {
91
+ if (!isFileNotFound(error)) {
92
+ throw new ConfigurationError(`Unable to reset application configuration: ${toError(error).message}`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ export function parseConfigValue(key, value) {
98
+ if (key === 'spotifyClientId') {
99
+ const clientId = spotifyClientIdSchema.safeParse(value);
100
+ if (clientId.success)
101
+ return clientId.data;
102
+ throw new ConfigurationError('Invalid Spotify client ID: expected a non-empty alphanumeric value.');
103
+ }
104
+ if (key === 'watchAfterPlay') {
105
+ if (value === 'true')
106
+ return true;
107
+ if (value === 'false')
108
+ return false;
109
+ throw new ConfigurationError('Invalid value for watchAfterPlay: expected either "true" or "false".');
110
+ }
111
+ if (key === 'refreshIntervalMs') {
112
+ if (/^-?\d+$/.test(value)) {
113
+ const interval = Number(value);
114
+ if (Number.isSafeInteger(interval) && interval >= 1_000 && interval <= 30_000) {
115
+ return interval;
116
+ }
117
+ }
118
+ throw new ConfigurationError('Invalid value for refreshIntervalMs: expected an integer from 1000 to 30000.');
119
+ }
120
+ throw new ConfigurationError(`Unknown configuration key: ${String(key)}.`);
121
+ }
122
+ function isFileNotFound(error) {
123
+ return (typeof error === 'object' &&
124
+ error !== null &&
125
+ 'code' in error &&
126
+ error.code === 'ENOENT');
127
+ }
128
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/storage/config.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ declare const credentialsSchema: z.ZodObject<{
3
3
  accessToken: z.ZodString;
4
4
  refreshToken: z.ZodString;
5
5
  expiresAt: z.ZodNumber;
6
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
7
  }, z.core.$strict>;
7
8
  export type Credentials = z.infer<typeof credentialsSchema>;
8
9
  export interface CredentialStore {
@@ -8,6 +8,7 @@ const credentialsSchema = z.strictObject({
8
8
  accessToken: z.string().min(1),
9
9
  refreshToken: z.string().min(1),
10
10
  expiresAt: z.number().int().nonnegative(),
11
+ scopes: z.array(z.string().min(1)).optional(),
11
12
  });
12
13
  export function getConfigDirectory(environment = process.env, homeDirectory = homedir()) {
13
14
  const xdgConfigHome = environment.XDG_CONFIG_HOME?.trim();
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/storage/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,iBAAiB,GAAG,CAAC,CAAC,YAAY,CAAC;IACvC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CAC1C,CAAC,CAAC;AAcH,MAAM,UAAU,kBAAkB,CAChC,cAAiC,OAAO,CAAC,GAAG,EAC5C,aAAa,GAAG,OAAO,EAAE;IAEzB,MAAM,aAAa,GAAG,WAAW,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC1D,OAAO,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,WAA2C,EAC3C,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAChB,QAAQ,GAAG,MAAM;IAEjB,OAAO,WAAW,CAAC,SAAS,IAAI,GAAG,GAAG,QAAQ,CAAC;AACjD,CAAC;AAED,MAAM,OAAO,mBAAmB;IACrB,IAAI,CAAS;IACL,SAAS,CAAS;IAEnC,YAAY,UAAsC,EAAE;QAClD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,EAAE,CAAC;QAC3D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,WAAwB;QAClC,MAAM,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC3D,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,IAAI,kBAAkB,CAAC,2CAA2C,CAAC,CAAC;QAC5E,CAAC;QAED,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,UAAU,EAAE,MAAM,CAAC,CAAC;QAE/E,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;gBAC7E,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YACH,MAAM,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAClC,MAAM,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACnD,MAAM,IAAI,kBAAkB,CAC1B,wCAAwC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACjE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,MAAM,IAAI,kBAAkB,CAC1B,8CAA8C,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACvE,CAAC;QACJ,CAAC;QAED,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAY,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,kBAAkB,CAAC,gDAAgD,CAAC,CAAC;QACjF,CAAC;QAED,MAAM,WAAW,GAAG,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACxD,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;YACzB,MAAM,IAAI,kBAAkB,CAAC,oDAAoD,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,WAAW,CAAC,IAAI,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,IAAY;QACnC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,kBAAkB,CAC1B,gDAAgD,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACzE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,KAAK,CAAC,IAAI,KAAK,QAAQ,CACxB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/storage/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,iBAAiB,GAAG,CAAC,CAAC,YAAY,CAAC;IACvC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACzC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAcH,MAAM,UAAU,kBAAkB,CAChC,cAAiC,OAAO,CAAC,GAAG,EAC5C,aAAa,GAAG,OAAO,EAAE;IAEzB,MAAM,aAAa,GAAG,WAAW,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC1D,OAAO,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,WAA2C,EAC3C,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,EAChB,QAAQ,GAAG,MAAM;IAEjB,OAAO,WAAW,CAAC,SAAS,IAAI,GAAG,GAAG,QAAQ,CAAC;AACjD,CAAC;AAED,MAAM,OAAO,mBAAmB;IACrB,IAAI,CAAS;IACL,SAAS,CAAS;IAEnC,YAAY,UAAsC,EAAE;QAClD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,EAAE,CAAC;QAC3D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,WAAwB;QAClC,MAAM,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC3D,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,IAAI,kBAAkB,CAAC,2CAA2C,CAAC,CAAC;QAC5E,CAAC;QAED,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,UAAU,EAAE,MAAM,CAAC,CAAC;QAE/E,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;gBAC7E,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YACH,MAAM,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAClC,MAAM,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACnD,MAAM,IAAI,kBAAkB,CAC1B,wCAAwC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACjE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,MAAM,IAAI,kBAAkB,CAC1B,8CAA8C,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACvE,CAAC;QACJ,CAAC;QAED,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAY,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,kBAAkB,CAAC,gDAAgD,CAAC,CAAC;QACjF,CAAC;QAED,MAAM,WAAW,GAAG,iBAAiB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACxD,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC;YACzB,MAAM,IAAI,kBAAkB,CAAC,oDAAoD,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,WAAW,CAAC,IAAI,CAAC;IAC1B,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,IAAY;QACnC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,kBAAkB,CAC1B,gDAAgD,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CACzE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,KAAK,CAAC,IAAI,KAAK,QAAQ,CACxB,CAAC;AACJ,CAAC"}
@@ -1,2 +1,4 @@
1
+ export { CONFIG_KEYS, DEFAULT_CONFIG, FileConfigStore, parseConfigValue, } from './config.js';
2
+ export type { AppConfig, ConfigKey, ConfigStore, FileConfigStoreOptions, } from './config.js';
1
3
  export { FileCredentialStore, getConfigDirectory, isTokenExpired, } from './credentials.js';
2
4
  export type { CredentialStore, Credentials, FileCredentialStoreOptions, } from './credentials.js';
@@ -1,2 +1,3 @@
1
+ export { CONFIG_KEYS, DEFAULT_CONFIG, FileConfigStore, parseConfigValue, } from './config.js';
1
2
  export { FileCredentialStore, getConfigDirectory, isTokenExpired, } from './credentials.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,cAAc,EACd,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAOrB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,29 @@
1
+ export interface SuccessfulUpdateCheckCache {
2
+ checkedAt: number;
3
+ currentVersion: string;
4
+ latestVersion: string;
5
+ }
6
+ export interface UpdateCache {
7
+ version: 1;
8
+ lastCheckedAt: number;
9
+ lastSuccessfulCheck?: SuccessfulUpdateCheckCache;
10
+ lastFailedCheckAt?: number;
11
+ }
12
+ export interface UpdateCacheStore {
13
+ read(): Promise<UpdateCache | null>;
14
+ write(cache: UpdateCache): Promise<void>;
15
+ }
16
+ export interface FileUpdateCacheStoreOptions {
17
+ directory?: string;
18
+ }
19
+ export declare class UpdateCacheError extends Error {
20
+ constructor(message: string, options?: ErrorOptions);
21
+ }
22
+ export declare class FileUpdateCacheStore implements UpdateCacheStore {
23
+ readonly path: string;
24
+ private readonly directory;
25
+ constructor(options?: FileUpdateCacheStoreOptions);
26
+ read(): Promise<UpdateCache | null>;
27
+ write(cache: UpdateCache): Promise<void>;
28
+ }
29
+ export declare function parseUpdateCache(value: unknown): UpdateCache;
@@ -0,0 +1,135 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { getConfigDirectory } from './credentials.js';
5
+ export class UpdateCacheError extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = 'UpdateCacheError';
9
+ }
10
+ }
11
+ export class FileUpdateCacheStore {
12
+ path;
13
+ directory;
14
+ constructor(options = {}) {
15
+ this.directory = options.directory ?? getConfigDirectory();
16
+ this.path = join(this.directory, 'update-cache.json');
17
+ }
18
+ async read() {
19
+ let contents;
20
+ try {
21
+ contents = await readFile(this.path, 'utf8');
22
+ }
23
+ catch (error) {
24
+ if (isFileNotFound(error))
25
+ return null;
26
+ throw new UpdateCacheError(`Unable to read the update cache: ${errorMessage(error)}`, {
27
+ cause: error,
28
+ });
29
+ }
30
+ try {
31
+ return parseUpdateCache(JSON.parse(contents));
32
+ }
33
+ catch {
34
+ // Cache data is disposable. Treat malformed or partially written data as a cache miss.
35
+ return null;
36
+ }
37
+ }
38
+ async write(cache) {
39
+ const validated = parseUpdateCache(cache);
40
+ try {
41
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
42
+ await chmod(this.directory, 0o700);
43
+ }
44
+ catch (error) {
45
+ throw new UpdateCacheError(`Unable to prepare the update cache directory: ${errorMessage(error)}`, {
46
+ cause: error,
47
+ });
48
+ }
49
+ const temporaryPath = join(this.directory, `.update-cache-${randomUUID()}.tmp`);
50
+ try {
51
+ await writeFile(temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, {
52
+ encoding: 'utf8',
53
+ flag: 'wx',
54
+ mode: 0o600,
55
+ });
56
+ await chmod(temporaryPath, 0o600);
57
+ await rename(temporaryPath, this.path);
58
+ await chmod(this.path, 0o600);
59
+ }
60
+ catch (error) {
61
+ await unlink(temporaryPath).catch(() => undefined);
62
+ throw new UpdateCacheError(`Unable to store the update cache: ${errorMessage(error)}`, {
63
+ cause: error,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ export function parseUpdateCache(value) {
69
+ if (!isRecord(value) || !hasOnlyKeys(value, CACHE_KEYS)) {
70
+ throw new UpdateCacheError('The update cache has an invalid format.');
71
+ }
72
+ if (value.version !== 1 || !isTimestamp(value.lastCheckedAt)) {
73
+ throw new UpdateCacheError('The update cache has an invalid format.');
74
+ }
75
+ const successful = parseSuccessfulCheck(value.lastSuccessfulCheck);
76
+ const failedAt = value.lastFailedCheckAt;
77
+ if (failedAt !== undefined && !isTimestamp(failedAt)) {
78
+ throw new UpdateCacheError('The update cache has an invalid format.');
79
+ }
80
+ if (!successful && failedAt === undefined) {
81
+ throw new UpdateCacheError('The update cache has an invalid format.');
82
+ }
83
+ const newestOutcomeAt = Math.max(successful?.checkedAt ?? -1, failedAt ?? -1);
84
+ if (value.lastCheckedAt !== newestOutcomeAt) {
85
+ throw new UpdateCacheError('The update cache has inconsistent timestamps.');
86
+ }
87
+ return {
88
+ version: 1,
89
+ lastCheckedAt: value.lastCheckedAt,
90
+ ...(successful ? { lastSuccessfulCheck: successful } : {}),
91
+ ...(failedAt === undefined ? {} : { lastFailedCheckAt: failedAt }),
92
+ };
93
+ }
94
+ const CACHE_KEYS = new Set([
95
+ 'version',
96
+ 'lastCheckedAt',
97
+ 'lastSuccessfulCheck',
98
+ 'lastFailedCheckAt',
99
+ ]);
100
+ const SUCCESS_KEYS = new Set(['checkedAt', 'currentVersion', 'latestVersion']);
101
+ function parseSuccessfulCheck(value) {
102
+ if (value === undefined)
103
+ return undefined;
104
+ if (!isRecord(value) ||
105
+ !hasOnlyKeys(value, SUCCESS_KEYS) ||
106
+ !isTimestamp(value.checkedAt) ||
107
+ !isNonemptyString(value.currentVersion) ||
108
+ !isNonemptyString(value.latestVersion)) {
109
+ throw new UpdateCacheError('The update cache has an invalid successful check.');
110
+ }
111
+ return {
112
+ checkedAt: value.checkedAt,
113
+ currentVersion: value.currentVersion,
114
+ latestVersion: value.latestVersion,
115
+ };
116
+ }
117
+ function isRecord(value) {
118
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
119
+ }
120
+ function hasOnlyKeys(value, allowed) {
121
+ return Object.keys(value).every((key) => allowed.has(key));
122
+ }
123
+ function isTimestamp(value) {
124
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
125
+ }
126
+ function isNonemptyString(value) {
127
+ return typeof value === 'string' && value.length > 0;
128
+ }
129
+ function isFileNotFound(error) {
130
+ return isRecord(error) && error.code === 'ENOENT';
131
+ }
132
+ function errorMessage(error) {
133
+ return error instanceof Error ? error.message : String(error);
134
+ }
135
+ //# sourceMappingURL=update-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-cache.js","sourceRoot":"","sources":["../../src/storage/update-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAwBtD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,MAAM,OAAO,oBAAoB;IACtB,IAAI,CAAS;IACL,SAAS,CAAS;IAEnC,YAAY,UAAuC,EAAE;QACnD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,kBAAkB,EAAE,CAAC;QAC3D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,MAAM,IAAI,gBAAgB,CAAC,oCAAoC,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE;gBACpF,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC;YACH,OAAO,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAY,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,uFAAuF;YACvF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAkB;QAC5B,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAE1C,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9D,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,gBAAgB,CAAC,iDAAiD,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE;gBACjG,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QAED,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,UAAU,EAAE,MAAM,CAAC,CAAC;QAChF,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;gBACxE,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;YACH,MAAM,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAClC,MAAM,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACnD,MAAM,IAAI,gBAAgB,CAAC,qCAAqC,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE;gBACrF,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,gBAAgB,CAAC,yCAAyC,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,gBAAgB,CAAC,yCAAyC,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,UAAU,GAAG,oBAAoB,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,KAAK,CAAC,iBAAiB,CAAC;IACzC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,gBAAgB,CAAC,yCAAyC,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,UAAU,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC1C,MAAM,IAAI,gBAAgB,CAAC,yCAAyC,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,IAAI,CAAC,CAAC,EAAE,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,aAAa,KAAK,eAAe,EAAE,CAAC;QAC5C,MAAM,IAAI,gBAAgB,CAAC,+CAA+C,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,OAAO,EAAE,CAAC;QACV,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,mBAAmB,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC;KACnE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,SAAS;IACT,eAAe;IACf,qBAAqB;IACrB,mBAAmB;CACpB,CAAC,CAAC;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC;AAE/E,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IACE,CAAC,QAAQ,CAAC,KAAK,CAAC;QAChB,CAAC,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC;QACjC,CAAC,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC;QAC7B,CAAC,gBAAgB,CAAC,KAAK,CAAC,cAAc,CAAC;QACvC,CAAC,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC,EACtC,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC,mDAAmD,CAAC,CAAC;IAClF,CAAC;IACD,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,aAAa,EAAE,KAAK,CAAC,aAAa;KACnC,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAA8B,EAAE,OAA4B;IAC/E,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACvD,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;AACpD,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { CurrentPlayback, Track } from '../services/models.js';
1
+ import type { Album, AlbumDetail, Artist, CurrentPlayback, Playlist, PlaylistDetail, RecentlyPlayedTrack, SavedTrack, Track } from '../services/models.js';
2
2
  export interface Output {
3
3
  log(message: string): void;
4
4
  error(message: string): void;
@@ -6,3 +6,12 @@ export interface Output {
6
6
  export declare const consoleOutput: Output;
7
7
  export declare function formatPlayback(playback: CurrentPlayback): string;
8
8
  export declare function formatTrack(track: Track, index?: number): string;
9
+ export declare function formatAlbum(album: Album, index?: number): string;
10
+ export declare function formatAlbumDetail(album: AlbumDetail): string;
11
+ export declare function formatArtist(artist: Artist, index?: number): string;
12
+ export declare function formatArtistDetail(artist: Artist): string;
13
+ export declare function formatPlaylist(playlist: Playlist, index?: number): string;
14
+ export declare function formatPlaylistOverview(playlist: Playlist): string;
15
+ export declare function formatPlaylistDetail(playlist: PlaylistDetail): string;
16
+ export declare function formatSavedTrack(item: SavedTrack, index?: number): string;
17
+ export declare function formatRecentlyPlayed(item: RecentlyPlayedTrack, index?: number): string;
package/dist/ui/output.js CHANGED
@@ -18,4 +18,64 @@ export function formatTrack(track, index) {
18
18
  const prefix = index === undefined ? '' : `${index + 1}. `;
19
19
  return `${prefix}${track.name} — ${track.artists.join(', ')} · ${track.album}`;
20
20
  }
21
+ export function formatAlbum(album, index) {
22
+ const prefix = index === undefined ? '' : `${index + 1}. `;
23
+ const release = album.releaseDate ? ` · ${album.releaseDate}` : '';
24
+ return `${prefix}${album.name} — ${album.artists.join(', ')} · ${album.totalTracks} tracks${release}`;
25
+ }
26
+ export function formatAlbumDetail(album) {
27
+ const lines = [formatAlbum(album), '', 'Tracks:'];
28
+ if (album.tracks.length === 0)
29
+ lines.push('No playable tracks found.');
30
+ else
31
+ lines.push(...album.tracks.map((track, index) => formatTrack(track, index)));
32
+ if (album.tracks.length < album.totalTracks) {
33
+ lines.push(`Showing ${album.tracks.length} of ${album.totalTracks} tracks.`);
34
+ }
35
+ if (album.externalUrl)
36
+ lines.push('', `Spotify: ${album.externalUrl}`);
37
+ return lines.join('\n');
38
+ }
39
+ export function formatArtist(artist, index) {
40
+ const prefix = index === undefined ? '' : `${index + 1}. `;
41
+ return `${prefix}${artist.name}`;
42
+ }
43
+ export function formatArtistDetail(artist) {
44
+ const lines = [formatArtist(artist)];
45
+ if (artist.externalUrl)
46
+ lines.push(`Spotify: ${artist.externalUrl}`);
47
+ return lines.join('\n');
48
+ }
49
+ export function formatPlaylist(playlist, index) {
50
+ const prefix = index === undefined ? '' : `${index + 1}. `;
51
+ const itemLabel = playlist.totalTracks === 1 ? 'item' : 'items';
52
+ return `${prefix}${playlist.name} — ${playlist.ownerName} · ${playlist.totalTracks} ${itemLabel}`;
53
+ }
54
+ export function formatPlaylistOverview(playlist) {
55
+ const lines = [formatPlaylist(playlist)];
56
+ if (playlist.description)
57
+ lines.push(playlist.description);
58
+ if (playlist.externalUrl)
59
+ lines.push(`Spotify: ${playlist.externalUrl}`);
60
+ return lines.join('\n');
61
+ }
62
+ export function formatPlaylistDetail(playlist) {
63
+ const lines = [formatPlaylist(playlist)];
64
+ if (playlist.description)
65
+ lines.push(playlist.description);
66
+ lines.push('', 'Tracks:');
67
+ if (playlist.tracks.length === 0)
68
+ lines.push('No playable tracks found.');
69
+ else
70
+ lines.push(...playlist.tracks.map((track, index) => formatTrack(track, index)));
71
+ if (playlist.externalUrl)
72
+ lines.push('', `Spotify: ${playlist.externalUrl}`);
73
+ return lines.join('\n');
74
+ }
75
+ export function formatSavedTrack(item, index) {
76
+ return `${formatTrack(item.track, index)} · liked ${item.addedAt}`;
77
+ }
78
+ export function formatRecentlyPlayed(item, index) {
79
+ return `${formatTrack(item.track, index)} · played ${item.playedAt}`;
80
+ }
21
81
  //# sourceMappingURL=output.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/ui/output.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOrE,MAAM,CAAC,MAAM,aAAa,GAAW;IACnC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IACtC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;CAC3C,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1E,OAAO;QACL,GAAG,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;QACrC,GAAG,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE;QACtC,EAAE;QACF,GAAG,cAAc,CAAC,QAAQ,CAAC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE;KACrI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,KAAc;IACtD,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,IAAI,CAAC;IAC3D,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;AACjF,CAAC"}
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/ui/output.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,13 @@
1
- import type { Track } from '../services/models.js';
1
+ import type { Album, Artist, Playlist, Track } from '../services/models.js';
2
2
  export declare function selectTrack(tracks: Track[]): Promise<Track | null>;
3
+ export declare function selectAlbum(albums: Album[]): Promise<Album | null>;
4
+ export declare function selectArtist(artists: Artist[]): Promise<Artist | null>;
5
+ export declare function selectPlaylist(playlists: Playlist[]): Promise<Playlist | null>;
6
+ export type AlbumAction = 'play-album' | 'play-track';
7
+ export type ArtistAction = 'play-artist' | 'select-album';
8
+ export type PlaylistAction = 'play-playlist';
9
+ export declare function selectAlbumAction(): Promise<AlbumAction | null>;
10
+ export declare function selectArtistAction(): Promise<ArtistAction | null>;
11
+ export declare function selectPlaylistAction(): Promise<PlaylistAction | null>;
12
+ export declare function promptSpotifyClientId(): Promise<string | null>;
13
+ export declare function confirmUpdate(currentVersion: string, latestVersion: string): Promise<boolean>;
@@ -1,19 +1,86 @@
1
1
  import { createInterface } from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
- import { formatTrack } from './output.js';
3
+ import { formatAlbum, formatArtist, formatPlaylist, formatTrack } from './output.js';
4
4
  export async function selectTrack(tracks) {
5
- if (tracks.length === 0)
5
+ return selectItem(tracks, formatTrack);
6
+ }
7
+ export async function selectAlbum(albums) {
8
+ return selectItem(albums, formatAlbum);
9
+ }
10
+ export async function selectArtist(artists) {
11
+ return selectItem(artists, formatArtist);
12
+ }
13
+ export async function selectPlaylist(playlists) {
14
+ return selectItem(playlists, formatPlaylist);
15
+ }
16
+ export async function selectAlbumAction() {
17
+ return selectAction([
18
+ { value: 'play-album', label: 'Play the entire album' },
19
+ { value: 'play-track', label: 'Select a track' },
20
+ ]);
21
+ }
22
+ export async function selectArtistAction() {
23
+ return selectAction([
24
+ { value: 'play-artist', label: 'Play the artist' },
25
+ { value: 'select-album', label: 'Select an album' },
26
+ ]);
27
+ }
28
+ export async function selectPlaylistAction() {
29
+ return selectAction([{ value: 'play-playlist', label: 'Play the playlist' }]);
30
+ }
31
+ export async function promptSpotifyClientId() {
32
+ if (!stdin.isTTY || !stdout.isTTY)
33
+ return null;
34
+ const prompt = createInterface({ input: stdin, output: stdout });
35
+ try {
36
+ const answer = await prompt.question('Spotify Client ID: ');
37
+ return answer.trim() || null;
38
+ }
39
+ finally {
40
+ prompt.close();
41
+ }
42
+ }
43
+ export async function confirmUpdate(currentVersion, latestVersion) {
44
+ if (!stdin.isTTY || !stdout.isTTY)
45
+ return false;
46
+ const prompt = createInterface({ input: stdin, output: stdout });
47
+ try {
48
+ const answer = await prompt.question(`Update spoti ${currentVersion} → ${latestVersion} with npm? [Y/n] `);
49
+ return answer.trim() === '' || /^y(?:es)?$/i.test(answer.trim());
50
+ }
51
+ finally {
52
+ prompt.close();
53
+ }
54
+ }
55
+ async function selectAction(actions) {
56
+ if (!stdin.isTTY || !stdout.isTTY)
57
+ return null;
58
+ stdout.write(`\nWhat do you want to play?\n\n${actions
59
+ .map((action, index) => `${index + 1}. ${action.label}`)
60
+ .join('\n')}\n${actions.length + 1}. Nothing\n\n`);
61
+ const prompt = createInterface({ input: stdin, output: stdout });
62
+ try {
63
+ const answer = await prompt.question(`Select [1-${actions.length + 1}]: `);
64
+ const index = Number.parseInt(answer, 10) - 1;
65
+ return actions[index]?.value ?? null;
66
+ }
67
+ finally {
68
+ prompt.close();
69
+ }
70
+ }
71
+ async function selectItem(items, format) {
72
+ if (items.length === 0)
6
73
  return null;
7
74
  if (!stdin.isTTY || !stdout.isTTY)
8
- return tracks[0] ?? null;
9
- stdout.write(`${tracks.map((track, index) => formatTrack(track, index)).join('\n')}\n\n`);
75
+ return items[0] ?? null;
76
+ stdout.write(`${items.map((item, index) => format(item, index)).join('\n')}\n\n`);
10
77
  const prompt = createInterface({ input: stdin, output: stdout });
11
78
  try {
12
- const answer = await prompt.question(`Select [1-${tracks.length}] (Enter to cancel): `);
79
+ const answer = await prompt.question(`Select [1-${items.length}] (Enter to cancel): `);
13
80
  if (!answer.trim())
14
81
  return null;
15
82
  const index = Number.parseInt(answer, 10) - 1;
16
- return tracks[index] ?? null;
83
+ return items[index] ?? null;
17
84
  }
18
85
  finally {
19
86
  prompt.close();
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/ui/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAe;IAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAE5D,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1F,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjE,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,aAAa,MAAM,CAAC,MAAM,uBAAuB,CAAC,CAAC;QACxF,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC;QAChC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QAC9C,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IAC/B,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/ui/prompts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { PlayerService } from '../services/player.service.js';
2
+ export interface WatchPlaybackOptions {
3
+ player: Pick<PlayerService, 'getCurrentPlayback'>;
4
+ refreshIntervalMs: number;
5
+ }
6
+ export type PlaybackWatcher = (options: WatchPlaybackOptions) => Promise<void>;
7
+ export declare const watchPlayback: PlaybackWatcher;
@@ -0,0 +1,44 @@
1
+ import { stdin, stdout } from 'node:process';
2
+ import { AppError } from '../utils/errors.js';
3
+ import { formatPlayback } from './output.js';
4
+ export const watchPlayback = async ({ player, refreshIntervalMs }) => {
5
+ if (!stdin.isTTY || !stdout.isTTY) {
6
+ throw new AppError('Watch mode requires an interactive terminal.');
7
+ }
8
+ let stopped = false;
9
+ let resolveStop = () => undefined;
10
+ const stopPromise = new Promise((resolve) => {
11
+ resolveStop = resolve;
12
+ });
13
+ const stop = () => {
14
+ stopped = true;
15
+ resolveStop();
16
+ };
17
+ process.once('SIGINT', stop);
18
+ stdout.write('\u001B[?1049h\u001B[?25l');
19
+ try {
20
+ while (!stopped) {
21
+ const playback = await player.getCurrentPlayback();
22
+ const content = playback
23
+ ? formatPlayback(playback)
24
+ : 'Nothing is currently playing.';
25
+ stdout.write(`\u001B[2J\u001B[H${content}\n\nPress Ctrl+C to exit`);
26
+ if (!stopped)
27
+ await waitForRefresh(refreshIntervalMs, stopPromise);
28
+ }
29
+ }
30
+ finally {
31
+ process.removeListener('SIGINT', stop);
32
+ stdout.write('\u001B[?25h\u001B[?1049l');
33
+ }
34
+ };
35
+ async function waitForRefresh(milliseconds, stopPromise) {
36
+ let timer;
37
+ const timerPromise = new Promise((resolve) => {
38
+ timer = setTimeout(resolve, milliseconds);
39
+ });
40
+ await Promise.race([timerPromise, stopPromise]);
41
+ if (timer)
42
+ clearTimeout(timer);
43
+ }
44
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/ui/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG7C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAS7C,MAAM,CAAC,MAAM,aAAa,GAAoB,KAAK,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE;IACpF,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClC,MAAM,IAAI,QAAQ,CAAC,8CAA8C,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,WAAW,GAAe,GAAG,EAAE,CAAC,SAAS,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAChD,WAAW,GAAG,OAAO,CAAC;IACxB,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,OAAO,GAAG,IAAI,CAAC;QACf,WAAW,EAAE,CAAC;IAChB,CAAC,CAAC;IAEF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAEzC,IAAI,CAAC;QACH,OAAO,CAAC,OAAO,EAAE,CAAC;YAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,kBAAkB,EAAE,CAAC;YACnD,MAAM,OAAO,GAAG,QAAQ;gBACtB,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC;gBAC1B,CAAC,CAAC,+BAA+B,CAAC;YACpC,MAAM,CAAC,KAAK,CAAC,oBAAoB,OAAO,0BAA0B,CAAC,CAAC;YACpE,IAAI,CAAC,OAAO;gBAAE,MAAM,cAAc,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC,CAAC;AAEF,KAAK,UAAU,cAAc,CAAC,YAAoB,EAAE,WAA0B;IAC5E,IAAI,KAAiC,CAAC;IACtC,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACjD,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC;IAChD,IAAI,KAAK;QAAE,YAAY,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC"}