morille 0.1.0 → 0.4.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 (134) hide show
  1. package/README.md +15 -3
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +2 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/components/animated-subtitle.d.ts +19 -0
  6. package/dist/components/animated-subtitle.d.ts.map +1 -0
  7. package/dist/components/animated-subtitle.js +109 -0
  8. package/dist/components/animated-subtitle.js.map +1 -0
  9. package/dist/components/animated-title.d.ts +16 -0
  10. package/dist/components/animated-title.d.ts.map +1 -0
  11. package/dist/components/animated-title.js +47 -0
  12. package/dist/components/animated-title.js.map +1 -0
  13. package/dist/components/browse-detail-view.d.ts.map +1 -0
  14. package/dist/components/browse-detail-view.js +2 -1
  15. package/dist/components/browse-detail-view.js.map +1 -0
  16. package/dist/components/device-indicator.d.ts +18 -0
  17. package/dist/components/device-indicator.d.ts.map +1 -0
  18. package/dist/components/device-indicator.js +53 -0
  19. package/dist/components/device-indicator.js.map +1 -0
  20. package/dist/components/device-picker-view.d.ts +10 -0
  21. package/dist/components/device-picker-view.d.ts.map +1 -0
  22. package/dist/components/device-picker-view.js +65 -0
  23. package/dist/components/device-picker-view.js.map +1 -0
  24. package/dist/components/lyrics-panel.d.ts.map +1 -0
  25. package/dist/components/lyrics-view.d.ts.map +1 -0
  26. package/dist/components/lyrics-view.js +2 -1
  27. package/dist/components/lyrics-view.js.map +1 -0
  28. package/dist/components/panel-content.d.ts.map +1 -0
  29. package/dist/components/panel-content.js +4 -0
  30. package/dist/components/panel-content.js.map +1 -0
  31. package/dist/components/playback-status.d.ts.map +1 -0
  32. package/dist/components/player.d.ts.map +1 -1
  33. package/dist/components/player.js +46 -10
  34. package/dist/components/player.js.map +1 -1
  35. package/dist/components/progress-bar.d.ts.map +1 -0
  36. package/dist/components/progress-bar.js +2 -1
  37. package/dist/components/progress-bar.js.map +1 -0
  38. package/dist/components/queue-view.d.ts.map +1 -0
  39. package/dist/components/queue-view.js +2 -1
  40. package/dist/components/queue-view.js.map +1 -0
  41. package/dist/components/search-panel.d.ts.map +1 -0
  42. package/dist/components/search-panel.js +3 -2
  43. package/dist/components/search-panel.js.map +1 -0
  44. package/dist/components/shimmer.d.ts.map +1 -0
  45. package/dist/components/shimmer.js +2 -2
  46. package/dist/components/shimmer.js.map +1 -0
  47. package/dist/components/track-facts.d.ts +13 -0
  48. package/dist/components/track-facts.d.ts.map +1 -0
  49. package/dist/components/track-facts.js +52 -0
  50. package/dist/components/track-facts.js.map +1 -0
  51. package/dist/components/track-info-skeleton.d.ts.map +1 -0
  52. package/dist/components/track-info.d.ts.map +1 -0
  53. package/dist/components/track-info.js +2 -1
  54. package/dist/components/track-info.js.map +1 -0
  55. package/dist/config.d.ts +3 -4
  56. package/dist/config.d.ts.map +1 -1
  57. package/dist/config.js +3 -4
  58. package/dist/config.js.map +1 -1
  59. package/dist/contexts/devices-context.d.ts +28 -0
  60. package/dist/contexts/devices-context.d.ts.map +1 -0
  61. package/dist/contexts/devices-context.js +33 -0
  62. package/dist/contexts/devices-context.js.map +1 -0
  63. package/dist/contexts/lyrics-context.d.ts.map +1 -0
  64. package/dist/contexts/panel-mode-context.d.ts +2 -1
  65. package/dist/contexts/panel-mode-context.d.ts.map +1 -0
  66. package/dist/contexts/panel-mode-context.js +3 -0
  67. package/dist/contexts/panel-mode-context.js.map +1 -0
  68. package/dist/contexts/queue-context.d.ts.map +1 -0
  69. package/dist/contexts/search-context.d.ts.map +1 -0
  70. package/dist/hooks/use-album-art.d.ts.map +1 -0
  71. package/dist/hooks/use-album-art.js +3 -1
  72. package/dist/hooks/use-album-art.js.map +1 -0
  73. package/dist/hooks/use-browse.d.ts.map +1 -0
  74. package/dist/hooks/use-browse.js +7 -3
  75. package/dist/hooks/use-browse.js.map +1 -0
  76. package/dist/hooks/use-devices.d.ts +24 -0
  77. package/dist/hooks/use-devices.d.ts.map +1 -0
  78. package/dist/hooks/use-devices.js +106 -0
  79. package/dist/hooks/use-devices.js.map +1 -0
  80. package/dist/hooks/use-lyrics.d.ts.map +1 -0
  81. package/dist/hooks/use-playback.d.ts +7 -0
  82. package/dist/hooks/use-playback.d.ts.map +1 -0
  83. package/dist/hooks/use-playback.js +59 -18
  84. package/dist/hooks/use-playback.js.map +1 -0
  85. package/dist/hooks/use-player-input.d.ts.map +1 -0
  86. package/dist/hooks/use-player-input.js +31 -2
  87. package/dist/hooks/use-player-input.js.map +1 -0
  88. package/dist/hooks/use-queue.d.ts.map +1 -0
  89. package/dist/hooks/use-queue.js +8 -3
  90. package/dist/hooks/use-queue.js.map +1 -0
  91. package/dist/hooks/use-search.d.ts.map +1 -0
  92. package/dist/hooks/use-search.js +2 -1
  93. package/dist/hooks/use-search.js.map +1 -0
  94. package/dist/icons.d.ts +29 -0
  95. package/dist/icons.d.ts.map +1 -0
  96. package/dist/icons.js +43 -0
  97. package/dist/icons.js.map +1 -0
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +4 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/logger.d.ts +9 -0
  103. package/dist/logger.d.ts.map +1 -0
  104. package/dist/logger.js +15 -0
  105. package/dist/logger.js.map +1 -0
  106. package/dist/main.d.ts.map +1 -0
  107. package/dist/spotify/auth.d.ts.map +1 -0
  108. package/dist/spotify/auth.js +3 -1
  109. package/dist/spotify/auth.js.map +1 -0
  110. package/dist/spotify/client.d.ts.map +1 -0
  111. package/dist/spotify/client.js +3 -1
  112. package/dist/spotify/client.js.map +1 -0
  113. package/dist/spotify/devices.d.ts +36 -0
  114. package/dist/spotify/devices.d.ts.map +1 -0
  115. package/dist/spotify/devices.js +72 -0
  116. package/dist/spotify/devices.js.map +1 -0
  117. package/dist/spotify/fetch-with-retry.d.ts.map +1 -0
  118. package/dist/spotify/fetch-with-retry.js +5 -4
  119. package/dist/spotify/fetch-with-retry.js.map +1 -0
  120. package/dist/spotify/lyrics.d.ts.map +1 -0
  121. package/dist/spotify/lyrics.js +5 -2
  122. package/dist/spotify/lyrics.js.map +1 -0
  123. package/dist/spotify/playback.d.ts +9 -0
  124. package/dist/spotify/playback.d.ts.map +1 -0
  125. package/dist/spotify/playback.js +40 -8
  126. package/dist/spotify/playback.js.map +1 -0
  127. package/dist/spotify/search.d.ts.map +1 -0
  128. package/dist/spotify/search.js +4 -4
  129. package/dist/spotify/search.js.map +1 -0
  130. package/dist/spotify/track-facts.d.ts +9 -0
  131. package/dist/spotify/track-facts.d.ts.map +1 -0
  132. package/dist/spotify/track-facts.js +95 -0
  133. package/dist/spotify/track-facts.js.map +1 -0
  134. package/package.json +21 -1
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { SEARCH_DEBOUNCE_MS } from '../config.js';
3
+ import { logger } from '../logger.js';
3
4
  import { searchSpotify } from '../spotify/search.js';
4
5
  /**
5
6
  * Manages search state with debounced API calls and deduplication.
@@ -26,7 +27,7 @@ export function useSearch(client, enabled) {
26
27
  }
27
28
  }
28
29
  catch (error) {
29
- console.error(`Search failed for query "${q}":`, error);
30
+ logger.error(`Search failed for query "${q}":`, error);
30
31
  }
31
32
  finally {
32
33
  if (inflightRef.current === q) {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-search.js","sourceRoot":"","sources":["../../src/hooks/use-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Centralised UI characters used across the player.
3
+ *
4
+ * Grouping all glyphs in one place makes it easy to swap them out for
5
+ * a different aesthetic, switch between ASCII and Unicode flavours,
6
+ * and audit which characters the app actually relies on (e.g. when
7
+ * checking which fonts/terminals will render correctly).
8
+ *
9
+ * Convention: trailing space is part of the icon when the icon is followed
10
+ * by a label on the same row, so consumers can do `{ICON}{label}` directly.
11
+ */
12
+ export declare const ICON_PLAYER_PLAY = "> ";
13
+ export declare const ICON_PLAYER_PAUSE = "|| ";
14
+ export declare const ICON_LIST_PLAYING = "\u25B6 ";
15
+ export declare const ICON_LIST_SELECTED = "> ";
16
+ export declare const ICON_LIST_GUTTER = " ";
17
+ export declare const ICON_INSTRUMENTAL = "\u266A";
18
+ export declare const ICON_BAR_FILLED = "\u2588";
19
+ export declare const ICON_BAR_EMPTY = "\u2591";
20
+ export declare const ICON_SHIMMER = "\u2591";
21
+ export declare const ICON_ARROW_VERTICAL = "\u2191/\u2193";
22
+ export declare const ICON_ARROW_HORIZONTAL = "\u2190/\u2192";
23
+ export declare const ICON_DEVICE_COMPUTER = "\u25AF";
24
+ export declare const ICON_DEVICE_PHONE = "\u25A2";
25
+ export declare const ICON_DEVICE_SPEAKER = "\u25C9";
26
+ export declare const ICON_DEVICE_TV = "\u25AD";
27
+ export declare const ICON_DEVICE_CAR = "\u25A3";
28
+ export declare const ICON_DEVICE_GAME = "\u25C6";
29
+ export declare const ICON_DEVICE_UNKNOWN = "\u00B7";
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAIvC,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAIrC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAG1C,eAAO,MAAM,eAAe,WAAW,CAAC;AACxC,eAAO,MAAM,cAAc,WAAW,CAAC;AAKvC,eAAO,MAAM,YAAY,WAAW,CAAC;AAGrC,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AAGrD,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAC1C,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAC5C,eAAO,MAAM,cAAc,WAAW,CAAC;AACvC,eAAO,MAAM,eAAe,WAAW,CAAC;AACxC,eAAO,MAAM,gBAAgB,WAAW,CAAC;AACzC,eAAO,MAAM,mBAAmB,WAAW,CAAC"}
package/dist/icons.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Centralised UI characters used across the player.
3
+ *
4
+ * Grouping all glyphs in one place makes it easy to swap them out for
5
+ * a different aesthetic, switch between ASCII and Unicode flavours,
6
+ * and audit which characters the app actually relies on (e.g. when
7
+ * checking which fonts/terminals will render correctly).
8
+ *
9
+ * Convention: trailing space is part of the icon when the icon is followed
10
+ * by a label on the same row, so consumers can do `{ICON}{label}` directly.
11
+ */
12
+ // ----- Player status (track-info header — global play/pause indicator) -----
13
+ // Kept ASCII so it renders cleanly even in terminals/fonts without media glyphs.
14
+ export const ICON_PLAYER_PLAY = '> ';
15
+ export const ICON_PLAYER_PAUSE = '|| ';
16
+ // ----- List item markers (queue, search results, browse drill-downs) -----
17
+ // Marker shown next to a track/album/playlist that matches current playback.
18
+ export const ICON_LIST_PLAYING = '\u25B6 '; // ▶
19
+ // Cursor shown next to the highlighted (selected) item in a navigable list.
20
+ export const ICON_LIST_SELECTED = '> ';
21
+ // Empty gutter for items that are neither selected nor playing — keeps columns aligned.
22
+ export const ICON_LIST_GUTTER = ' ';
23
+ // ----- Lyrics -----
24
+ // Placeholder shown for empty/instrumental lines in the synced view.
25
+ export const ICON_INSTRUMENTAL = '\u266A'; // ♪
26
+ // ----- Progress bar -----
27
+ export const ICON_BAR_FILLED = '\u2588'; // █
28
+ export const ICON_BAR_EMPTY = '\u2591'; // ░
29
+ // ----- Shimmer / skeleton placeholder -----
30
+ // Same glyph as ICON_BAR_EMPTY but kept as a separate constant so the two can
31
+ // diverge later without touching every consumer.
32
+ export const ICON_SHIMMER = '\u2591'; // ░
33
+ // ----- Keyboard hint labels -----
34
+ export const ICON_ARROW_VERTICAL = '\u2191/\u2193'; // ↑/↓
35
+ export const ICON_ARROW_HORIZONTAL = '\u2190/\u2192'; // ←/→
36
+ // ----- Device types (device picker + header indicator) -----
37
+ export const ICON_DEVICE_COMPUTER = '\u25AF'; // ▯
38
+ export const ICON_DEVICE_PHONE = '\u25A2'; // ▢
39
+ export const ICON_DEVICE_SPEAKER = '\u25C9'; // ◉
40
+ export const ICON_DEVICE_TV = '\u25AD'; // ▭
41
+ export const ICON_DEVICE_CAR = '\u25A3'; // ▣
42
+ export const ICON_DEVICE_GAME = '\u25C6'; // ◆
43
+ export const ICON_DEVICE_UNKNOWN = '\u00B7'; // ·
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8EAA8E;AAC9E,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AACrC,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC,4EAA4E;AAC5E,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC,CAAC,IAAI;AAChD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AACvC,wFAAwF;AACxF,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,qBAAqB;AACrB,qEAAqE;AACrE,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,CAAC,IAAI;AAE/C,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC,CAAC,IAAI;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,IAAI;AAE5C,6CAA6C;AAC7C,8EAA8E;AAC9E,iDAAiD;AACjD,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,IAAI;AAE1C,mCAAmC;AACnC,MAAM,CAAC,MAAM,mBAAmB,GAAG,eAAe,CAAC,CAAC,MAAM;AAC1D,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC,CAAC,MAAM;AAE5D,8DAA8D;AAC9D,MAAM,CAAC,MAAM,oBAAoB,GAAG,QAAQ,CAAC,CAAC,IAAI;AAClD,MAAM,CAAC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,CAAC,IAAI;AAC/C,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAAC,IAAI;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,IAAI;AAC5C,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC,CAAC,IAAI;AAC7C,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,CAAC,IAAI;AAC9C,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAAC,IAAI"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
+ import './logger.js';
2
3
  export type { RepeatMode, TrackInfo } from './spotify/playback.js';
3
4
  export { getCurrentTrackInfo, getPlaybackState, pause, play, playInContext, playUri, seek, setRepeat, setShuffle, setVolume, skipNext, skipPrevious, togglePlayback, } from './spotify/playback.js';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,aAAa,CAAC;AASrB,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,IAAI,EACJ,aAAa,EACb,OAAO,EACP,IAAI,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,cAAc,GACf,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,10 @@
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { loadEnvFile } from 'node:process';
5
+ // Side-effect import: captures the `--debug` flag from `process.argv` at
6
+ // module load before anything else observes argv. Must come first.
7
+ import './logger.js';
8
+ process.argv = process.argv.filter((arg) => arg !== '--debug');
5
9
  const envPath = join(import.meta.dirname, '../.env');
6
10
  if (existsSync(envPath)) {
7
11
  loadEnvFile(envPath);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,yEAAyE;AACzE,mEAAmE;AACnE,OAAO,aAAa,CAAC;AAErB,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC;AAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACrD,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IACxB,WAAW,CAAC,OAAO,CAAC,CAAC;AACvB,CAAC;AAGD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,EACL,IAAI,EACJ,aAAa,EACb,OAAO,EACP,IAAI,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,cAAc,GACf,MAAM,uBAAuB,CAAC;AAE/B,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Single sanctioned sink for stderr output in morille. All `console.error`
3
+ * calls across the codebase go through this object. When neither the
4
+ * `--debug` CLI flag nor the `DEBUG=1` env var is set, `logger.error` is a
5
+ * no-op so the ink TUI stays silent.
6
+ */
7
+ export declare const logger: {
8
+ error: (...args: unknown[]) => void;
9
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,eAAO,MAAM,MAAM;IACjB,KAAK,YAAY,OAAO,EAAE,KAAG,IAAI;CAIlC,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,15 @@
1
+ // biome-ignore lint/complexity/useLiteralKeys: TS noPropertyAccessFromIndexSignature requires bracket notation
2
+ const debugEnabled = process.argv.includes('--debug') || process.env['DEBUG'] === '1';
3
+ /**
4
+ * Single sanctioned sink for stderr output in morille. All `console.error`
5
+ * calls across the codebase go through this object. When neither the
6
+ * `--debug` CLI flag nor the `DEBUG=1` env var is set, `logger.error` is a
7
+ * no-op so the ink TUI stays silent.
8
+ */
9
+ export const logger = {
10
+ error: (...args) => {
11
+ if (!debugEnabled)
12
+ return;
13
+ console.error(...args);
14
+ },
15
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,GAAG,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,CAAC,GAAG,IAAe,EAAQ,EAAE;QAClC,IAAI,CAAC,YAAY;YAAE,OAAO;QAC1B,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACzB,CAAC;CACF,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.tsx"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/spotify/auth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAgH3D,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC9D,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAoBxG;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAYpD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAKnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAK1D;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CA6B7F"}
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from 'node:crypto';
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { createServer } from 'node:http';
5
5
  import { AUTH_REDIRECT_PORT, AUTH_REDIRECT_URI, AUTH_SCOPES, CONFIG_DIR, TOKEN_PATH } from '../config.js';
6
+ import { logger } from '../logger.js';
6
7
  function generateCodeVerifier() {
7
8
  return randomBytes(64).toString('base64url');
8
9
  }
@@ -124,7 +125,8 @@ export function loadStoredToken() {
124
125
  const data = readFileSync(TOKEN_PATH, 'utf-8');
125
126
  return JSON.parse(data);
126
127
  }
127
- catch {
128
+ catch (err) {
129
+ logger.error(`Failed to read stored token at ${TOKEN_PATH}:`, err);
128
130
  return null;
129
131
  }
130
132
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/spotify/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/spotify/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAGrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAIrD;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAapC;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoB9G"}
@@ -1,5 +1,6 @@
1
1
  import { SpotifyApi } from '@spotify/web-api-ts-sdk';
2
2
  import { DEFAULT_SPOTIFY_CLIENT_ID } from '../config.js';
3
+ import { logger } from '../logger.js';
3
4
  import { authenticate, isTokenExpired, loadStoredToken, refreshToken, storeToken } from './auth.js';
4
5
  import { fetchWithRetry } from './fetch-with-retry.js';
5
6
  /**
@@ -34,7 +35,8 @@ export async function createSpotifyClient(clientId, options) {
34
35
  try {
35
36
  token = await refreshToken(clientId, token);
36
37
  }
37
- catch {
38
+ catch (err) {
39
+ logger.error('Token refresh failed, falling back to full re-auth:', err);
38
40
  token = null;
39
41
  }
40
42
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/spotify/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACpG,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW;IACzB,+GAA+G;IAC/G,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,OAAO,EAAE,IAAI,EAAE,IAAI,yBAAyB,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,mCAAmC;YACjC,oEAAoE;YACpE,wEAAwE;YACxE,mDAAmD,CACtD,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB,EAAE,OAA6B;IACvF,IAAI,KAAK,GAAG,eAAe,EAAE,CAAC;IAE9B,IAAI,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QAC1D,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,KAAK,CAAC,qDAAqD,EAAE,GAAG,CAAC,CAAC;YACzE,KAAK,GAAG,IAAI,CAAC;QACf,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,KAAK,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC9C,UAAU,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,UAAU,CAAC,eAAe,CAAC,QAAQ,EAAE,KAAK,EAAE;QACjD,KAAK,EAAE,cAAc;KACtB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { SpotifyApi } from '@spotify/web-api-ts-sdk';
2
+ export type DeviceType = 'Computer' | 'Smartphone' | 'Speaker' | 'TV' | 'AVR' | 'STB' | 'AudioDongle' | 'GameConsole' | 'CastVideo' | 'CastAudio' | 'Automobile' | 'Unknown';
3
+ export type SpotifyDevice = {
4
+ id: string;
5
+ name: string;
6
+ type: DeviceType;
7
+ isActive: boolean;
8
+ isRestricted: boolean;
9
+ volumePercent: number | null;
10
+ };
11
+ /**
12
+ * Narrows the SDK's `string` device type to the `DeviceType` union,
13
+ * falling back to `'Unknown'` for any value Spotify adds later that we do
14
+ * not recognise yet.
15
+ */
16
+ export declare function normalizeDeviceType(value: string | null | undefined): DeviceType;
17
+ /**
18
+ * Lists every Spotify Connect device visible to the current user. Ghost
19
+ * entries with `id === null` (briefly returned during state transitions)
20
+ * are filtered out so callers never have to handle them.
21
+ * @param client - Authenticated Spotify API client
22
+ * @returns Array of devices ready to display and target
23
+ */
24
+ export declare function fetchDevices(client: SpotifyApi): Promise<SpotifyDevice[]>;
25
+ /**
26
+ * Transfers playback to a target device via the Spotify Connect API.
27
+ * The SDK's deserializer calls JSON.parse on any non-204 response body, and
28
+ * this endpoint sometimes returns 200 with a non-JSON body — the transfer
29
+ * still succeeds server-side, so the resulting SyntaxError is swallowed
30
+ * (same behavior as `playback.ts#ignoreSdkParseError`).
31
+ * @param client - Authenticated Spotify API client
32
+ * @param deviceId - Target device ID
33
+ * @param resume - When true, playback resumes on the new device; when false,
34
+ * the session is transferred but the playing state is preserved
35
+ */
36
+ export declare function transferPlayback(client: SpotifyApi, deviceId: string, resume: boolean): Promise<void>;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.d.ts","sourceRoot":"","sources":["../../src/spotify/devices.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,YAAY,GACZ,SAAS,GACT,IAAI,GACJ,KAAK,GACL,KAAK,GACL,aAAa,GACb,aAAa,GACb,WAAW,GACX,WAAW,GACX,YAAY,GACZ,SAAS,CAAC;AAEd,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAiBF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,CAKhF;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAe/E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3G"}
@@ -0,0 +1,72 @@
1
+ const KNOWN_DEVICE_TYPES = new Set([
2
+ 'Computer',
3
+ 'Smartphone',
4
+ 'Speaker',
5
+ 'TV',
6
+ 'AVR',
7
+ 'STB',
8
+ 'AudioDongle',
9
+ 'GameConsole',
10
+ 'CastVideo',
11
+ 'CastAudio',
12
+ 'Automobile',
13
+ 'Unknown',
14
+ ]);
15
+ /**
16
+ * Narrows the SDK's `string` device type to the `DeviceType` union,
17
+ * falling back to `'Unknown'` for any value Spotify adds later that we do
18
+ * not recognise yet.
19
+ */
20
+ export function normalizeDeviceType(value) {
21
+ if (value && KNOWN_DEVICE_TYPES.has(value)) {
22
+ return value;
23
+ }
24
+ return 'Unknown';
25
+ }
26
+ /**
27
+ * Lists every Spotify Connect device visible to the current user. Ghost
28
+ * entries with `id === null` (briefly returned during state transitions)
29
+ * are filtered out so callers never have to handle them.
30
+ * @param client - Authenticated Spotify API client
31
+ * @returns Array of devices ready to display and target
32
+ */
33
+ export async function fetchDevices(client) {
34
+ const response = await client.player.getAvailableDevices();
35
+ const devices = [];
36
+ for (const raw of response.devices) {
37
+ if (!raw.id)
38
+ continue;
39
+ devices.push({
40
+ id: raw.id,
41
+ name: raw.name,
42
+ type: normalizeDeviceType(raw.type),
43
+ isActive: raw.is_active,
44
+ isRestricted: raw.is_restricted,
45
+ volumePercent: raw.volume_percent,
46
+ });
47
+ }
48
+ return devices;
49
+ }
50
+ /**
51
+ * Transfers playback to a target device via the Spotify Connect API.
52
+ * The SDK's deserializer calls JSON.parse on any non-204 response body, and
53
+ * this endpoint sometimes returns 200 with a non-JSON body — the transfer
54
+ * still succeeds server-side, so the resulting SyntaxError is swallowed
55
+ * (same behavior as `playback.ts#ignoreSdkParseError`).
56
+ * @param client - Authenticated Spotify API client
57
+ * @param deviceId - Target device ID
58
+ * @param resume - When true, playback resumes on the new device; when false,
59
+ * the session is transferred but the playing state is preserved
60
+ */
61
+ export async function transferPlayback(client, deviceId, resume) {
62
+ try {
63
+ await client.player.transferPlayback([
64
+ deviceId,
65
+ ], resume);
66
+ }
67
+ catch (err) {
68
+ if (err instanceof SyntaxError)
69
+ return;
70
+ throw err;
71
+ }
72
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/spotify/devices.ts"],"names":[],"mappings":"AAyBA,MAAM,kBAAkB,GAA4B,IAAI,GAAG,CAAa;IACtE,UAAU;IACV,YAAY;IACZ,SAAS;IACT,IAAI;IACJ,KAAK;IACL,KAAK;IACL,aAAa;IACb,aAAa;IACb,WAAW;IACX,WAAW;IACX,YAAY;IACZ,SAAS;CACV,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAgC;IAClE,IAAI,KAAK,IAAK,kBAA0C,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACpE,OAAO,KAAmB,CAAC;IAC7B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAkB;IACnD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnC,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,SAAS;QACtB,OAAO,CAAC,IAAI,CAAC;YACX,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,IAAI,EAAE,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,YAAY,EAAE,GAAG,CAAC,aAAa;YAC/B,aAAa,EAAE,GAAG,CAAC,cAAc;SAClC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAkB,EAAE,QAAgB,EAAE,MAAe;IAC1F,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAClC;YACE,QAAQ;SACT,EACD,MAAM,CACP,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,WAAW;YAAE,OAAO;QACvC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-with-retry.d.ts","sourceRoot":"","sources":["../../src/spotify/fetch-with-retry.ts"],"names":[],"mappings":"AA8BA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwB9F"}
@@ -1,4 +1,5 @@
1
1
  import { USER_AGENT } from '../config.js';
2
+ import { logger } from '../logger.js';
2
3
  const MAX_RETRIES = 3;
3
4
  const DEFAULT_RETRY_AFTER_S = 1;
4
5
  const MAX_RETRY_AFTER_S = 5;
@@ -30,11 +31,11 @@ function withUserAgent(init) {
30
31
  export function fetchWithRetry(input, init) {
31
32
  const initWithUa = withUserAgent(init);
32
33
  const key = getCacheKey(input, initWithUa);
33
- console.error(`fetchWithRetry: ${key}`); // Log all fetch attempts for debugging
34
+ logger.error(`fetchWithRetry: ${key}`);
34
35
  // Only deduplicate GET requests - mutations must always go through
35
36
  const method = initWithUa.method ?? 'GET';
36
37
  if (method !== 'GET') {
37
- console.error(`fetchWithRetry: Body`, initWithUa.body); // Log request body for non-GET requests
38
+ logger.error(`fetchWithRetry: Body`, initWithUa.body);
38
39
  return attempt(input, initWithUa, MAX_RETRIES);
39
40
  }
40
41
  const pending = inflight.get(key);
@@ -55,7 +56,7 @@ async function attempt(input, init, retryCount) {
55
56
  // Calling `.text()` on the original response would leave the body unusable
56
57
  // and trigger "Body is unusable: Body has already been read" downstream.
57
58
  const bodyText = await response.clone().text();
58
- console.error(`Request failed with status ${response.status} for ${input.toString()}`, bodyText);
59
+ logger.error(`Request failed with status ${response.status} for ${input.toString()}`, bodyText);
59
60
  }
60
61
  if (response.status !== 429 || retryCount >= MAX_RETRIES) {
61
62
  return response;
@@ -64,7 +65,7 @@ async function attempt(input, init, retryCount) {
64
65
  const retryAfterS = retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : Number.NaN;
65
66
  const clampedS = Number.isNaN(retryAfterS) ? DEFAULT_RETRY_AFTER_S : Math.min(retryAfterS, MAX_RETRY_AFTER_S);
66
67
  const delayMs = clampedS * 1000;
67
- console.error(`Received 429 for ${input.toString()}. Retrying after ${clampedS} seconds (retry ${retryCount + 1}/${MAX_RETRIES})`);
68
+ logger.error(`Received 429 for ${input.toString()}. Retrying after ${clampedS} seconds (retry ${retryCount + 1}/${MAX_RETRIES})`);
68
69
  await new Promise((resolve) => setTimeout(resolve, delayMs));
69
70
  return attempt(input, init, retryCount + 1);
70
71
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-with-retry.js","sourceRoot":"","sources":["../../src/spotify/fetch-with-retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lyrics.d.ts","sourceRoot":"","sources":["../../src/spotify/lyrics.ts"],"names":[],"mappings":"AAgCA,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IACnB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AA8CF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAc7G;AAiCD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAUhG"}
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { CONFIG_DIR } from '../config.js';
5
+ import { logger } from '../logger.js';
5
6
  import { fetchWithRetry } from './fetch-with-retry.js';
6
7
  const LYRICS_CACHE_DIR = join(CONFIG_DIR, 'lyrics-cache');
7
8
  function getCachePath(trackName, artistName) {
@@ -15,7 +16,8 @@ function readDiskCache(trackName, artistName) {
15
16
  try {
16
17
  return JSON.parse(readFileSync(path, 'utf-8'));
17
18
  }
18
- catch {
19
+ catch (err) {
20
+ logger.error(`Failed to read lyrics cache at ${path}:`, err);
19
21
  return null;
20
22
  }
21
23
  }
@@ -106,7 +108,8 @@ async function doFetch(trackName, artistName, durationMs) {
106
108
  writeDiskCache(trackName, artistName, lyrics);
107
109
  return lyrics;
108
110
  }
109
- catch {
111
+ catch (err) {
112
+ logger.error(`Failed to fetch lyrics for "${trackName}" by "${artistName}":`, err);
110
113
  return null;
111
114
  }
112
115
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lyrics.js","sourceRoot":"","sources":["../../src/spotify/lyrics.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { PlaybackState, SpotifyApi } from '@spotify/web-api-ts-sdk';
2
+ import type { DeviceType } from './devices.js';
2
3
  export type RepeatMode = 'off' | 'context' | 'track';
3
4
  export type PlaybackContext = {
4
5
  type: string;
@@ -12,12 +13,20 @@ export type TrackInfo = {
12
13
  progressMs: number;
13
14
  durationMs: number;
14
15
  deviceId: string | null;
16
+ deviceName: string | null;
17
+ deviceType: DeviceType | null;
15
18
  volume: number | null;
16
19
  shuffle: boolean;
17
20
  repeat: RepeatMode;
18
21
  albumImageUrl: string | null;
19
22
  context: PlaybackContext | null;
20
23
  uri: string;
24
+ releaseYear: number | null;
25
+ popularity: number | null;
26
+ explicit: boolean;
27
+ artistCount: number;
28
+ albumTotalTracks: number | null;
29
+ isrcCountry: string | null;
21
30
  };
22
31
  /**
23
32
  * Fetches the current playback state from Spotify.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback.d.ts","sourceRoot":"","sources":["../../src/spotify/playback.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { logger } from '../logger.js';
2
+ import { normalizeDeviceType } from './devices.js';
1
3
  /**
2
4
  * Fetches the current playback state from Spotify.
3
5
  * @param client - Authenticated Spotify API client
@@ -135,10 +137,10 @@ async function ignoreSdkParseError(fn) {
135
137
  }
136
138
  // Translate the SDK's misleading 403 message for player restriction errors
137
139
  if (err instanceof Error && err.message.includes('Restriction violated')) {
138
- console.error('Action failed due to Spotify playback restrictions:', err);
140
+ logger.error('Action failed due to Spotify playback restrictions:', err);
139
141
  throw new Error('Action not available for the current playback context');
140
142
  }
141
- console.error('Unexpected error during playback control:', err);
143
+ logger.error('Unexpected error during playback control:', err);
142
144
  throw err;
143
145
  }
144
146
  }
@@ -164,6 +166,30 @@ export async function togglePlayback(client) {
164
166
  await play(client, deviceId);
165
167
  return true;
166
168
  }
169
+ function extractTrackMetadata(item) {
170
+ const artist = 'artists' in item ? item.artists.map((a) => a.name).join(', ') : 'Unknown';
171
+ const album = 'album' in item ? item.album.name : '';
172
+ const albumImageUrl = 'album' in item && item.album.images.length > 0 ? item.album.images[item.album.images.length - 1].url : null;
173
+ const parsedYear = 'album' in item ? Number.parseInt(item.album.release_date.slice(0, 4), 10) : Number.NaN;
174
+ const releaseYear = Number.isFinite(parsedYear) ? parsedYear : null;
175
+ const popularity = 'popularity' in item ? item.popularity : null;
176
+ const explicit = 'explicit' in item ? item.explicit : false;
177
+ const artistCount = 'artists' in item ? item.artists.length : 0;
178
+ const albumTotalTracks = 'album' in item ? item.album.total_tracks : null;
179
+ const isrcRaw = 'external_ids' in item ? item.external_ids?.isrc : undefined;
180
+ const isrcCountry = isrcRaw && isrcRaw.length >= 2 ? isrcRaw.slice(0, 2).toUpperCase() : null;
181
+ return {
182
+ artist,
183
+ album,
184
+ albumImageUrl,
185
+ releaseYear,
186
+ popularity,
187
+ explicit,
188
+ artistCount,
189
+ albumTotalTracks,
190
+ isrcCountry,
191
+ };
192
+ }
167
193
  /**
168
194
  * Fetches simplified info about the currently playing track.
169
195
  * @param client - Authenticated Spotify API client
@@ -175,21 +201,21 @@ export async function getCurrentTrackInfo(client) {
175
201
  return null;
176
202
  }
177
203
  const { item } = state;
178
- const artist = 'artists' in item ? item.artists.map((a) => a.name).join(', ') : 'Unknown';
179
- const album = 'album' in item ? item.album.name : '';
180
- const albumImageUrl = 'album' in item && item.album.images.length > 0 ? item.album.images[item.album.images.length - 1].url : null;
204
+ const meta = extractTrackMetadata(item);
181
205
  return {
182
206
  name: item.name,
183
- artist,
184
- album,
207
+ artist: meta.artist,
208
+ album: meta.album,
185
209
  isPlaying: state.is_playing,
186
210
  progressMs: state.progress_ms,
187
211
  durationMs: item.duration_ms,
188
212
  deviceId: state.device.id,
213
+ deviceName: state.device.name ?? null,
214
+ deviceType: state.device.type ? normalizeDeviceType(state.device.type) : null,
189
215
  volume: state.device.volume_percent,
190
216
  shuffle: state.shuffle_state,
191
217
  repeat: state.repeat_state,
192
- albumImageUrl,
218
+ albumImageUrl: meta.albumImageUrl,
193
219
  context: state.context
194
220
  ? {
195
221
  type: state.context.type,
@@ -197,5 +223,11 @@ export async function getCurrentTrackInfo(client) {
197
223
  }
198
224
  : null,
199
225
  uri: item.uri,
226
+ releaseYear: meta.releaseYear,
227
+ popularity: meta.popularity,
228
+ explicit: meta.explicit,
229
+ artistCount: meta.artistCount,
230
+ albumTotalTracks: meta.albumTotalTracks,
231
+ isrcCountry: meta.isrcCountry,
200
232
  };
201
233
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback.js","sourceRoot":"","sources":["../../src/spotify/playback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/spotify/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAG1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,SAAS,EAAE,oBAAoB,EAAE,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB,CAAC;AAaF,wBAAsB,aAAa,CACjC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAA6B,GACnC,OAAO,CAAC,aAAa,CAAC,CA0ExB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAchG;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAmBzG;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,UAAU,EAClB,KAAK,GAAE,MAA6B,EACpC,MAAM,GAAE,MAAU,GACjB,OAAO,CAAC;IACT,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAiBD"}
@@ -6,11 +6,11 @@ import { SEARCH_RESULTS_LIMIT, USER_PLAYLISTS_LIMIT } from '../config.js';
6
6
  * @param limit - Max results per category (default: SEARCH_RESULTS_LIMIT)
7
7
  * @returns Categorized search results
8
8
  */
9
- // Multi-type search reliably rejects values above this with `400 Invalid limit`,
10
- // even though the Spotify docs claim `max=50` for single-type searches.
11
- const MULTI_TYPE_SEARCH_MAX_LIMIT = 20;
9
+ // Spotify's live API rejects values above 10 with `400 Invalid limit`,
10
+ // even though older docs claim `max=50`.
11
+ const SEARCH_MAX_LIMIT = 10;
12
12
  export async function searchSpotify(client, query, limit = SEARCH_RESULTS_LIMIT) {
13
- const safeLimit = Math.max(1, Math.min(limit, MULTI_TYPE_SEARCH_MAX_LIMIT));
13
+ const safeLimit = Math.max(1, Math.min(limit, SEARCH_MAX_LIMIT));
14
14
  const data = await client.search(query, [
15
15
  'track',
16
16
  'album',