@involvex/youtube-music-cli 0.0.46 → 0.0.48

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 (118) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/cli.js.map +1004 -0
  3. package/dist/source/hooks/usePlayer.d.ts +1 -0
  4. package/dist/source/services/player-state/player-state.service.d.ts +1 -0
  5. package/dist/source/stores/player.store.d.ts +1 -0
  6. package/dist/source/types/actions.d.ts +4 -0
  7. package/dist/source/types/player.types.d.ts +3 -2
  8. package/dist/source/utils/constants.d.ts +1 -0
  9. package/dist/source/utils/icons.d.ts +1 -0
  10. package/dist/youtube-music-cli +0 -0
  11. package/package.json +1 -1
  12. package/dist/eslint.config.js +0 -55
  13. package/dist/package.json +0 -120
  14. package/dist/scripts/build-cli.js +0 -46
  15. package/dist/source/app.js +0 -17
  16. package/dist/source/cli.js +0 -504
  17. package/dist/source/components/common/ErrorBoundary.js +0 -22
  18. package/dist/source/components/common/Help.js +0 -18
  19. package/dist/source/components/common/ShortcutsBar.js +0 -80
  20. package/dist/source/components/config/ConfigLayout.js +0 -84
  21. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  22. package/dist/source/components/export/ExportLayout.js +0 -111
  23. package/dist/source/components/import/ImportLayout.js +0 -119
  24. package/dist/source/components/import/ImportProgress.js +0 -73
  25. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  26. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  27. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  28. package/dist/source/components/layouts/MainLayout.js +0 -190
  29. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  30. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  31. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  32. package/dist/source/components/layouts/SearchLayout.js +0 -193
  33. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  34. package/dist/source/components/player/NowPlaying.js +0 -45
  35. package/dist/source/components/player/PlayerControls.js +0 -83
  36. package/dist/source/components/player/ProgressBar.js +0 -19
  37. package/dist/source/components/player/QueueList.js +0 -36
  38. package/dist/source/components/player/Suggestions.js +0 -50
  39. package/dist/source/components/playlist/PlaylistList.js +0 -138
  40. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  41. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  42. package/dist/source/components/plugins/PluginsList.js +0 -18
  43. package/dist/source/components/search/SearchBar.js +0 -55
  44. package/dist/source/components/search/SearchHistory.js +0 -35
  45. package/dist/source/components/search/SearchResults.js +0 -280
  46. package/dist/source/components/settings/Settings.js +0 -211
  47. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  48. package/dist/source/config/themes.config.js +0 -123
  49. package/dist/source/contexts/theme.context.js +0 -29
  50. package/dist/source/hooks/useKeyboard.js +0 -188
  51. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  52. package/dist/source/hooks/useNavigation.js +0 -5
  53. package/dist/source/hooks/usePlayer.js +0 -43
  54. package/dist/source/hooks/usePlaylist.js +0 -65
  55. package/dist/source/hooks/useSearch.js +0 -76
  56. package/dist/source/hooks/useSleepTimer.js +0 -48
  57. package/dist/source/hooks/useTerminalSize.js +0 -24
  58. package/dist/source/hooks/useTheme.js +0 -5
  59. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  60. package/dist/source/main.js +0 -127
  61. package/dist/source/services/cache/cache.service.js +0 -67
  62. package/dist/source/services/completions/completions.service.js +0 -313
  63. package/dist/source/services/config/config.service.js +0 -191
  64. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  65. package/dist/source/services/download/download.service.js +0 -350
  66. package/dist/source/services/export/export.service.js +0 -131
  67. package/dist/source/services/history/history.service.js +0 -83
  68. package/dist/source/services/import/import.service.js +0 -272
  69. package/dist/source/services/import/spotify.service.js +0 -171
  70. package/dist/source/services/import/track-matcher.service.js +0 -271
  71. package/dist/source/services/import/youtube-import.service.js +0 -84
  72. package/dist/source/services/logger/logger.service.js +0 -52
  73. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  74. package/dist/source/services/mpris/mpris.service.js +0 -78
  75. package/dist/source/services/notification/notification.service.js +0 -57
  76. package/dist/source/services/player/dependency-check.service.js +0 -140
  77. package/dist/source/services/player/player.service.js +0 -478
  78. package/dist/source/services/player-state/player-state.service.js +0 -122
  79. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  80. package/dist/source/services/plugin/plugin-context.js +0 -256
  81. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  82. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  83. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  84. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  85. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  86. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  87. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  88. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  89. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  90. package/dist/source/services/version-check/version-check.service.js +0 -121
  91. package/dist/source/services/web/static-file.service.js +0 -185
  92. package/dist/source/services/web/web-server-manager.js +0 -506
  93. package/dist/source/services/web/web-streaming.service.js +0 -290
  94. package/dist/source/services/web/websocket.server.js +0 -267
  95. package/dist/source/services/youtube-music/api.js +0 -649
  96. package/dist/source/services/youtube-music/search.service.js +0 -38
  97. package/dist/source/stores/history.store.js +0 -64
  98. package/dist/source/stores/navigation.store.js +0 -90
  99. package/dist/source/stores/player.store.js +0 -724
  100. package/dist/source/stores/plugins.store.js +0 -177
  101. package/dist/source/types/actions.js +0 -1
  102. package/dist/source/types/cli.types.js +0 -1
  103. package/dist/source/types/config.types.js +0 -1
  104. package/dist/source/types/history.types.js +0 -1
  105. package/dist/source/types/import.types.js +0 -2
  106. package/dist/source/types/keyboard.types.js +0 -1
  107. package/dist/source/types/navigation.types.js +0 -1
  108. package/dist/source/types/player.types.js +0 -1
  109. package/dist/source/types/playlist.types.js +0 -1
  110. package/dist/source/types/plugin.types.js +0 -1
  111. package/dist/source/types/theme.types.js +0 -1
  112. package/dist/source/types/web.types.js +0 -2
  113. package/dist/source/types/youtube-music.types.js +0 -1
  114. package/dist/source/types/youtubei.types.js +0 -3
  115. package/dist/source/utils/constants.js +0 -134
  116. package/dist/source/utils/format.js +0 -24
  117. package/dist/source/utils/icons.js +0 -26
  118. package/dist/source/utils/search-filters.js +0 -100
@@ -1,271 +0,0 @@
1
- import { getMusicService } from "../youtube-music/api.js";
2
- import { logger } from "../logger/logger.service.js";
3
- class TrackMatcherService {
4
- musicService = getMusicService();
5
- searchCache = new Map();
6
- matchCache = new Map();
7
- /**
8
- * Build a search query from track metadata
9
- */
10
- buildSearchQuery(track) {
11
- // Combine artist and title for best results
12
- const artists = track.artists.slice(0, 2).join(', '); // Use up to 2 artists
13
- const name = track.name || track.title;
14
- return `${artists} ${name}`.trim();
15
- }
16
- /**
17
- * Get track name from either type
18
- */
19
- getTrackName(track) {
20
- return track.name || track.title;
21
- }
22
- /**
23
- * Calculate string similarity using Levenshtein distance
24
- */
25
- calculateSimilarity(str1, str2) {
26
- const s1 = str1.toLowerCase().normalize();
27
- const s2 = str2.toLowerCase().normalize();
28
- // Exact match
29
- if (s1 === s2)
30
- return 1;
31
- // Check if one contains the other
32
- if (s1.includes(s2) || s2.includes(s1))
33
- return 0.9;
34
- // Simple Levenshtein-based similarity
35
- const longer = s1.length > s2.length ? s1 : s2;
36
- const shorter = s1.length > s2.length ? s2 : s1;
37
- if (longer.length === 0)
38
- return 1;
39
- const editDistance = this.levenshteinDistance(longer, shorter);
40
- return (longer.length - editDistance) / longer.length;
41
- }
42
- /**
43
- * Calculate Levenshtein distance between two strings
44
- */
45
- levenshteinDistance(str1, str2) {
46
- const matrix = [];
47
- for (let i = 0; i <= str2.length; i++) {
48
- matrix[i] = [i];
49
- }
50
- for (let j = 0; j <= str1.length; j++) {
51
- if (matrix[0]) {
52
- matrix[0][j] = j;
53
- }
54
- }
55
- for (let i = 1; i <= str2.length; i++) {
56
- const row = matrix[i];
57
- if (!row)
58
- continue;
59
- const prevRow = matrix[i - 1];
60
- if (!prevRow)
61
- continue;
62
- for (let j = 1; j <= str1.length; j++) {
63
- if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
64
- row[j] = prevRow[j - 1] ?? 0;
65
- }
66
- else {
67
- row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, // substitution
68
- (row[j - 1] ?? 0) + 1, // insertion
69
- (prevRow[j] ?? 0) + 1);
70
- }
71
- }
72
- }
73
- const lastRow = matrix[str2.length];
74
- return lastRow?.[str1.length] ?? 0;
75
- }
76
- /**
77
- * Check if artists match (any overlap in artist lists)
78
- */
79
- artistsMatch(trackArtists, candidateArtists) {
80
- const normalizedTrackArtists = trackArtists
81
- .map(a => a.toLowerCase().trim())
82
- .filter(a => a.length > 0);
83
- // candidateArtists are strings directly from the track
84
- const normalizedCandidateArtists = candidateArtists
85
- .map(a => a.toLowerCase().trim())
86
- .filter(a => a.length > 0);
87
- // Check for any artist name overlap
88
- return normalizedTrackArtists.some(trackArtist => normalizedCandidateArtists.some(candidateArtist => candidateArtist.includes(trackArtist) ||
89
- trackArtist.includes(candidateArtist)));
90
- }
91
- /**
92
- * Calculate duration proximity score (0-1)
93
- */
94
- durationScore(originalDuration, candidateDuration) {
95
- if (!originalDuration || !candidateDuration)
96
- return 0.5; // Neutral if either is missing
97
- const diff = Math.abs(originalDuration - candidateDuration);
98
- const maxDiff = Math.max(originalDuration, candidateDuration) * 0.3; // 30% tolerance
99
- if (diff === 0)
100
- return 1;
101
- if (diff <= maxDiff)
102
- return 1 - diff / maxDiff;
103
- return 0;
104
- }
105
- /**
106
- * Score a track match based on multiple factors
107
- */
108
- scoreMatch(original, candidate) {
109
- let score = 0;
110
- const weights = {
111
- title: 0.5,
112
- artist: 0.3,
113
- duration: 0.2,
114
- };
115
- // Title similarity
116
- const titleSimilarity = this.calculateSimilarity(this.getTrackName(original), candidate.title);
117
- score += titleSimilarity * weights.title;
118
- // Artist match
119
- const artistMatch = this.artistsMatch(original.artists, candidate.artists.map(a => a.name))
120
- ? 1
121
- : 0;
122
- score += artistMatch * weights.artist;
123
- // Duration proximity
124
- const durationScore = this.durationScore(original.duration, candidate.duration ?? 0);
125
- score += durationScore * weights.duration;
126
- return score;
127
- }
128
- /**
129
- * Determine confidence level from score
130
- */
131
- getConfidence(score) {
132
- if (score >= 0.85)
133
- return 'high';
134
- if (score >= 0.7)
135
- return 'medium';
136
- if (score >= 0.5)
137
- return 'low';
138
- return 'none';
139
- }
140
- /**
141
- * Search YouTube Music for a track
142
- */
143
- async searchTrack(track) {
144
- const query = this.buildSearchQuery(track);
145
- const cacheKey = `track:${query}`;
146
- // Check cache first
147
- const cached = this.searchCache.get(cacheKey);
148
- if (cached) {
149
- logger.debug('TrackMatcherService', 'Using cached search results', {
150
- query,
151
- });
152
- return cached;
153
- }
154
- try {
155
- logger.debug('TrackMatcherService', 'Searching for track', { query });
156
- const response = await this.musicService.search(query, {
157
- type: 'songs',
158
- limit: 10,
159
- });
160
- const results = response.results
161
- .filter(r => r.type === 'song')
162
- .map(r => r.data);
163
- // Cache results
164
- this.searchCache.set(cacheKey, results);
165
- return results;
166
- }
167
- catch (error) {
168
- logger.error('TrackMatcherService', 'Track search failed', {
169
- query,
170
- error: error instanceof Error ? error.message : String(error),
171
- });
172
- return [];
173
- }
174
- }
175
- /**
176
- * Find the best matching track on YouTube Music
177
- */
178
- async findMatch(original) {
179
- // Create a unique key for this track
180
- const trackKey = `${original.artists[0] ?? ''}-${this.getTrackName(original)}-${original.duration}`;
181
- const cached = this.matchCache.get(trackKey);
182
- if (cached) {
183
- logger.debug('TrackMatcherService', 'Using cached match', {
184
- track: this.getTrackName(original),
185
- });
186
- return cached;
187
- }
188
- try {
189
- const candidates = await this.searchTrack(original);
190
- if (candidates.length === 0) {
191
- const noMatch = {
192
- originalTrack: original,
193
- matchedTrack: null,
194
- confidence: 'none',
195
- };
196
- this.matchCache.set(trackKey, noMatch);
197
- return noMatch;
198
- }
199
- // Score all candidates
200
- const scoredCandidates = candidates.map(candidate => ({
201
- ...candidate,
202
- score: this.scoreMatch(original, candidate),
203
- }));
204
- // Sort by score descending
205
- scoredCandidates.sort((a, b) => b.score - a.score);
206
- const best = scoredCandidates[0];
207
- if (!best) {
208
- const noMatch = {
209
- originalTrack: original,
210
- matchedTrack: null,
211
- confidence: 'none',
212
- };
213
- this.matchCache.set(trackKey, noMatch);
214
- return noMatch;
215
- }
216
- const { score, ...matchedTrack } = best;
217
- const confidence = this.getConfidence(score);
218
- const match = {
219
- originalTrack: original,
220
- matchedTrack: matchedTrack,
221
- confidence,
222
- };
223
- logger.debug('TrackMatcherService', 'Track matched', {
224
- original: this.getTrackName(original),
225
- matched: matchedTrack.title,
226
- confidence,
227
- score,
228
- });
229
- this.matchCache.set(trackKey, match);
230
- return match;
231
- }
232
- catch (error) {
233
- logger.error('TrackMatcherService', 'Match finding failed', {
234
- track: this.getTrackName(original),
235
- error: error instanceof Error ? error.message : String(error),
236
- });
237
- const errorMatch = {
238
- originalTrack: original,
239
- matchedTrack: null,
240
- confidence: 'none',
241
- error: error instanceof Error ? error.message : String(error),
242
- };
243
- return errorMatch;
244
- }
245
- }
246
- /**
247
- * Clear all caches
248
- */
249
- clearCache() {
250
- this.searchCache.clear();
251
- this.matchCache.clear();
252
- logger.debug('TrackMatcherService', 'Caches cleared');
253
- }
254
- /**
255
- * Get cache statistics
256
- */
257
- getCacheStats() {
258
- return {
259
- searchCache: this.searchCache.size,
260
- matchCache: this.matchCache.size,
261
- };
262
- }
263
- }
264
- // Singleton instance
265
- let trackMatcherServiceInstance = null;
266
- export function getTrackMatcherService() {
267
- if (!trackMatcherServiceInstance) {
268
- trackMatcherServiceInstance = new TrackMatcherService();
269
- }
270
- return trackMatcherServiceInstance;
271
- }
@@ -1,84 +0,0 @@
1
- import { getMusicService } from "../youtube-music/api.js";
2
- import { logger } from "../logger/logger.service.js";
3
- class YouTubeImportService {
4
- /**
5
- * Extract playlist ID from various YouTube URL formats
6
- */
7
- extractPlaylistId(input) {
8
- // Direct playlist ID
9
- if (/^[-_A-Za-z0-9]{10,}$/.test(input)) {
10
- return input;
11
- }
12
- // youtube.com/watch?v=...&list=...
13
- const watchMatch = input.match(/[?&]list=([-_A-Za-z0-9]+)/);
14
- if (watchMatch) {
15
- return watchMatch[1] ?? null;
16
- }
17
- // youtube.com/playlist?list=...
18
- const playlistMatch = input.match(/[?&]list=([-_A-Za-z0-9]+)/);
19
- if (playlistMatch) {
20
- return playlistMatch[1] ?? null;
21
- }
22
- return null;
23
- }
24
- /**
25
- * Fetch a YouTube playlist and normalize to import format
26
- */
27
- async fetchPlaylist(urlOrId) {
28
- const playlistId = this.extractPlaylistId(urlOrId);
29
- if (!playlistId) {
30
- logger.warn('YouTubeImportService', 'Invalid YouTube playlist URL or ID', {
31
- input: urlOrId,
32
- });
33
- return null;
34
- }
35
- try {
36
- logger.info('YouTubeImportService', 'Fetching YouTube playlist', {
37
- playlistId,
38
- });
39
- const musicService = getMusicService();
40
- const playlist = await musicService.getPlaylist(playlistId);
41
- // Normalize to YouTubePlaylist import format
42
- const normalized = {
43
- id: playlist.playlistId,
44
- name: playlist.name,
45
- tracks: playlist.tracks.map(track => ({
46
- id: track.videoId,
47
- title: track.title,
48
- name: track.title,
49
- artists: track.artists.map(a => a.name),
50
- album: track.album?.name,
51
- duration: track.duration ?? 0,
52
- })),
53
- url: `https://www.youtube.com/playlist?list=${playlist.playlistId}`,
54
- };
55
- logger.info('YouTubeImportService', 'Successfully fetched playlist', {
56
- playlistId,
57
- trackCount: normalized.tracks.length,
58
- });
59
- return normalized;
60
- }
61
- catch (error) {
62
- logger.error('YouTubeImportService', 'Failed to fetch playlist', {
63
- playlistId,
64
- error: error instanceof Error ? error.message : String(error),
65
- });
66
- return null;
67
- }
68
- }
69
- /**
70
- * Validate if a playlist is accessible (not private/unavailable)
71
- */
72
- async validatePlaylist(urlOrId) {
73
- const playlist = await this.fetchPlaylist(urlOrId);
74
- return playlist !== null && playlist.tracks.length > 0;
75
- }
76
- }
77
- // Singleton instance
78
- let youtubeImportServiceInstance = null;
79
- export function getYouTubeImportService() {
80
- if (!youtubeImportServiceInstance) {
81
- youtubeImportServiceInstance = new YouTubeImportService();
82
- }
83
- return youtubeImportServiceInstance;
84
- }
@@ -1,52 +0,0 @@
1
- // Debug logging service
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
- import * as os from 'node:os';
5
- const DEBUG_DIR = path.join(os.homedir(), '.youtube-music-cli');
6
- const DEBUG_FILE = path.join(DEBUG_DIR, 'debug.log');
7
- const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
8
- // Ensure debug directory exists
9
- if (!fs.existsSync(DEBUG_DIR)) {
10
- fs.mkdirSync(DEBUG_DIR, { recursive: true });
11
- }
12
- // Rotate log if too large
13
- if (fs.existsSync(DEBUG_FILE)) {
14
- const stats = fs.statSync(DEBUG_FILE);
15
- if (stats.size > MAX_LOG_SIZE) {
16
- const backupFile = path.join(DEBUG_DIR, 'debug.log.old');
17
- if (fs.existsSync(backupFile)) {
18
- fs.unlinkSync(backupFile);
19
- }
20
- fs.renameSync(DEBUG_FILE, backupFile);
21
- }
22
- }
23
- class Logger {
24
- writeToFile(level, category, message, data) {
25
- const timestamp = new Date().toISOString();
26
- const dataStr = data ? `\n${JSON.stringify(data, null, 2)}` : '';
27
- const logLine = `[${timestamp}] [${level}] [${category}] ${message}${dataStr}\n`;
28
- fs.appendFileSync(DEBUG_FILE, logLine);
29
- }
30
- debug(category, message, data) {
31
- this.writeToFile('DEBUG', category, message, data);
32
- }
33
- info(category, message, data) {
34
- this.writeToFile('INFO', category, message, data);
35
- // Disabled: console.log causes Ink to re-render constantly
36
- // console.log(`[${category}] ${message}`);
37
- }
38
- warn(category, message, data) {
39
- this.writeToFile('WARN', category, message, data);
40
- // Disabled: console.warn causes Ink to re-render
41
- // console.warn(`[${category}] ${message}`);
42
- }
43
- error(category, message, data) {
44
- this.writeToFile('ERROR', category, message, data);
45
- // Keep console.error for critical errors, but this should be rare
46
- console.error(`[${category}] ${message}`);
47
- }
48
- getLogPath() {
49
- return DEBUG_FILE;
50
- }
51
- }
52
- export const logger = new Logger();
@@ -1,93 +0,0 @@
1
- // Lyrics service using LRCLIB API (https://lrclib.net)
2
- // Free, no authentication required
3
- import { logger } from "../logger/logger.service.js";
4
- const LRCLIB_BASE = 'https://lrclib.net/api';
5
- class LyricsService {
6
- static instance;
7
- cache = new Map();
8
- constructor() { }
9
- static getInstance() {
10
- if (!LyricsService.instance) {
11
- LyricsService.instance = new LyricsService();
12
- }
13
- return LyricsService.instance;
14
- }
15
- /** Parse LRC format into timed lines */
16
- parseLrc(lrc) {
17
- const lines = [];
18
- const lineRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/;
19
- for (const rawLine of lrc.split('\n')) {
20
- const match = lineRegex.exec(rawLine.trim());
21
- if (match) {
22
- const minutes = Number.parseInt(match[1], 10);
23
- const seconds = Number.parseInt(match[2], 10);
24
- const centiseconds = Number.parseInt(match[3].padEnd(3, '0'), 10);
25
- const time = minutes * 60 + seconds + centiseconds / 1000;
26
- const text = match[4].trim();
27
- lines.push({ time, text });
28
- }
29
- }
30
- return lines.sort((a, b) => a.time - b.time);
31
- }
32
- async getLyrics(trackName, artistName, duration) {
33
- const cacheKey = `${trackName}::${artistName}`;
34
- if (this.cache.has(cacheKey)) {
35
- return this.cache.get(cacheKey) ?? null;
36
- }
37
- try {
38
- const params = new URLSearchParams({
39
- track_name: trackName,
40
- artist_name: artistName,
41
- ...(duration ? { duration: String(Math.round(duration)) } : {}),
42
- });
43
- const response = await fetch(`${LRCLIB_BASE}/get?${params.toString()}`);
44
- if (!response.ok) {
45
- if (response.status === 404) {
46
- logger.debug('LyricsService', 'No lyrics found', {
47
- trackName,
48
- artistName,
49
- });
50
- this.cache.set(cacheKey, null);
51
- return null;
52
- }
53
- throw new Error(`LRCLIB API error: ${response.status}`);
54
- }
55
- const data = (await response.json());
56
- const lyrics = {
57
- synced: data.syncedLyrics ? this.parseLrc(data.syncedLyrics) : null,
58
- plain: data.plainLyrics ?? null,
59
- };
60
- this.cache.set(cacheKey, lyrics);
61
- logger.info('LyricsService', 'Lyrics loaded', {
62
- trackName,
63
- hasSynced: !!lyrics.synced,
64
- hasPlain: !!lyrics.plain,
65
- });
66
- return lyrics;
67
- }
68
- catch (error) {
69
- logger.warn('LyricsService', 'Failed to fetch lyrics', {
70
- error: error instanceof Error ? error.message : String(error),
71
- });
72
- this.cache.set(cacheKey, null);
73
- return null;
74
- }
75
- }
76
- /** Get the current lyric line index based on playback position */
77
- getCurrentLineIndex(lines, currentTime) {
78
- let index = 0;
79
- for (let i = 0; i < lines.length; i++) {
80
- if (lines[i].time <= currentTime) {
81
- index = i;
82
- }
83
- else {
84
- break;
85
- }
86
- }
87
- return index;
88
- }
89
- clearCache() {
90
- this.cache.clear();
91
- }
92
- }
93
- export const getLyricsService = () => LyricsService.getInstance();
@@ -1,78 +0,0 @@
1
- // MPRIS service — Linux only, enables playerctl / media key support
2
- // No-ops on non-Linux platforms
3
- import { logger } from "../logger/logger.service.js";
4
- export class MprisService {
5
- player = null;
6
- get isSupported() {
7
- return process.platform === 'linux';
8
- }
9
- async initialize(callbacks = {}) {
10
- if (!this.isSupported) {
11
- logger.debug('MprisService', 'MPRIS not supported on this platform', {
12
- platform: process.platform,
13
- });
14
- return;
15
- }
16
- try {
17
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18
- // @ts-ignore
19
- const mpris = (await import('mpris-service'));
20
- this.player = mpris.createPlayer({
21
- name: 'youtube-music-cli',
22
- identity: 'YouTube Music CLI',
23
- supportedInterfaces: ['player'],
24
- });
25
- // Wire up MPRIS events to player callbacks
26
- if (callbacks.onPlay)
27
- this.player.on('play', callbacks.onPlay);
28
- if (callbacks.onPause)
29
- this.player.on('pause', callbacks.onPause);
30
- if (callbacks.onNext)
31
- this.player.on('next', callbacks.onNext);
32
- if (callbacks.onPrevious)
33
- this.player.on('previous', callbacks.onPrevious);
34
- this.player.canPlay = true;
35
- this.player.canPause = true;
36
- this.player.canGoNext = true;
37
- this.player.canGoPrevious = true;
38
- this.player.canSeek = false;
39
- logger.info('MprisService', 'MPRIS player initialized');
40
- }
41
- catch (error) {
42
- logger.warn('MprisService', 'Could not initialize MPRIS', {
43
- error: error instanceof Error ? error.message : String(error),
44
- });
45
- }
46
- }
47
- updateTrack(track, isPlaying) {
48
- if (!this.player)
49
- return;
50
- try {
51
- this.player.metadata = {
52
- 'mpris:length': track.duration,
53
- 'xesam:title': track.title,
54
- 'xesam:artist': [track.artist],
55
- };
56
- this.player.playbackStatus = isPlaying ? 'Playing' : 'Paused';
57
- }
58
- catch {
59
- // Ignore MPRIS update errors
60
- }
61
- }
62
- setPlaying(playing) {
63
- if (!this.player)
64
- return;
65
- try {
66
- this.player.playbackStatus = playing ? 'Playing' : 'Paused';
67
- }
68
- catch {
69
- // Ignore
70
- }
71
- }
72
- }
73
- let instance = null;
74
- export const getMprisService = () => {
75
- if (!instance)
76
- instance = new MprisService();
77
- return instance;
78
- };
@@ -1,57 +0,0 @@
1
- // Desktop notification service
2
- import { logger } from "../logger/logger.service.js";
3
- class NotificationService {
4
- static instance;
5
- enabled = false;
6
- notifier = null;
7
- constructor() { }
8
- static getInstance() {
9
- if (!NotificationService.instance) {
10
- NotificationService.instance = new NotificationService();
11
- }
12
- return NotificationService.instance;
13
- }
14
- setEnabled(enabled) {
15
- this.enabled = enabled;
16
- }
17
- isEnabled() {
18
- return this.enabled;
19
- }
20
- async getNotifier() {
21
- if (!this.notifier) {
22
- // Lazy-load to avoid startup cost when disabled
23
- const mod = await import('node-notifier');
24
- this.notifier = mod.default;
25
- }
26
- return this.notifier;
27
- }
28
- async notify(title, message) {
29
- if (!this.enabled)
30
- return;
31
- try {
32
- const notifier = await this.getNotifier();
33
- notifier.notify({
34
- title,
35
- message,
36
- sound: false,
37
- wait: false,
38
- }, (error) => {
39
- if (error) {
40
- logger.warn('NotificationService', 'Notification failed', {
41
- error: error.message,
42
- });
43
- }
44
- });
45
- }
46
- catch (error) {
47
- // Gracefully handle if notifications aren't supported
48
- logger.warn('NotificationService', 'Failed to send notification', {
49
- error: error instanceof Error ? error.message : String(error),
50
- });
51
- }
52
- }
53
- async notifyTrackChange(title, artist) {
54
- await this.notify('Now Playing', `${title} — ${artist}`);
55
- }
56
- }
57
- export const getNotificationService = () => NotificationService.getInstance();