@involvex/youtube-music-cli 0.0.47 → 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 (111) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/cli.js.map +3 -3
  3. package/dist/youtube-music-cli +0 -0
  4. package/package.json +1 -1
  5. package/dist/eslint.config.js +0 -55
  6. package/dist/package.json +0 -120
  7. package/dist/scripts/build-cli.js +0 -46
  8. package/dist/source/app.js +0 -17
  9. package/dist/source/cli.js +0 -504
  10. package/dist/source/components/common/ErrorBoundary.js +0 -22
  11. package/dist/source/components/common/Help.js +0 -18
  12. package/dist/source/components/common/ShortcutsBar.js +0 -89
  13. package/dist/source/components/config/ConfigLayout.js +0 -84
  14. package/dist/source/components/config/KeybindingsLayout.js +0 -107
  15. package/dist/source/components/export/ExportLayout.js +0 -111
  16. package/dist/source/components/import/ImportLayout.js +0 -119
  17. package/dist/source/components/import/ImportProgress.js +0 -73
  18. package/dist/source/components/layouts/ExploreLayout.js +0 -72
  19. package/dist/source/components/layouts/HistoryLayout.js +0 -37
  20. package/dist/source/components/layouts/LyricsLayout.js +0 -89
  21. package/dist/source/components/layouts/MainLayout.js +0 -190
  22. package/dist/source/components/layouts/MiniPlayerLayout.js +0 -20
  23. package/dist/source/components/layouts/PlayerLayout.js +0 -9
  24. package/dist/source/components/layouts/PluginsLayout.js +0 -77
  25. package/dist/source/components/layouts/SearchLayout.js +0 -193
  26. package/dist/source/components/layouts/TrendingLayout.js +0 -59
  27. package/dist/source/components/player/NowPlaying.js +0 -45
  28. package/dist/source/components/player/PlayerControls.js +0 -83
  29. package/dist/source/components/player/ProgressBar.js +0 -19
  30. package/dist/source/components/player/QueueList.js +0 -36
  31. package/dist/source/components/player/Suggestions.js +0 -50
  32. package/dist/source/components/playlist/PlaylistList.js +0 -138
  33. package/dist/source/components/plugins/PluginInstallDialog.js +0 -41
  34. package/dist/source/components/plugins/PluginsAvailable.js +0 -55
  35. package/dist/source/components/plugins/PluginsList.js +0 -18
  36. package/dist/source/components/search/SearchBar.js +0 -55
  37. package/dist/source/components/search/SearchHistory.js +0 -35
  38. package/dist/source/components/search/SearchResults.js +0 -280
  39. package/dist/source/components/settings/Settings.js +0 -211
  40. package/dist/source/components/theme/ThemeSwitcher.js +0 -11
  41. package/dist/source/config/themes.config.js +0 -123
  42. package/dist/source/contexts/theme.context.js +0 -29
  43. package/dist/source/hooks/useKeyboard.js +0 -188
  44. package/dist/source/hooks/useKeyboardBlocker.js +0 -45
  45. package/dist/source/hooks/useNavigation.js +0 -5
  46. package/dist/source/hooks/usePlayer.js +0 -43
  47. package/dist/source/hooks/usePlaylist.js +0 -65
  48. package/dist/source/hooks/useSearch.js +0 -76
  49. package/dist/source/hooks/useSleepTimer.js +0 -48
  50. package/dist/source/hooks/useTerminalSize.js +0 -24
  51. package/dist/source/hooks/useTheme.js +0 -5
  52. package/dist/source/hooks/useYouTubeMusic.js +0 -112
  53. package/dist/source/main.js +0 -127
  54. package/dist/source/services/cache/cache.service.js +0 -67
  55. package/dist/source/services/completions/completions.service.js +0 -313
  56. package/dist/source/services/config/config.service.js +0 -191
  57. package/dist/source/services/discord/discord-rpc.service.js +0 -95
  58. package/dist/source/services/download/download.service.js +0 -350
  59. package/dist/source/services/export/export.service.js +0 -131
  60. package/dist/source/services/history/history.service.js +0 -83
  61. package/dist/source/services/import/import.service.js +0 -272
  62. package/dist/source/services/import/spotify.service.js +0 -171
  63. package/dist/source/services/import/track-matcher.service.js +0 -271
  64. package/dist/source/services/import/youtube-import.service.js +0 -84
  65. package/dist/source/services/logger/logger.service.js +0 -52
  66. package/dist/source/services/lyrics/lyrics.service.js +0 -93
  67. package/dist/source/services/mpris/mpris.service.js +0 -78
  68. package/dist/source/services/notification/notification.service.js +0 -57
  69. package/dist/source/services/player/dependency-check.service.js +0 -140
  70. package/dist/source/services/player/player.service.js +0 -478
  71. package/dist/source/services/player-state/player-state.service.js +0 -123
  72. package/dist/source/services/plugin/plugin-audio-api.js +0 -36
  73. package/dist/source/services/plugin/plugin-context.js +0 -256
  74. package/dist/source/services/plugin/plugin-hooks.service.js +0 -135
  75. package/dist/source/services/plugin/plugin-installer.service.js +0 -248
  76. package/dist/source/services/plugin/plugin-loader.service.js +0 -161
  77. package/dist/source/services/plugin/plugin-permissions.service.js +0 -194
  78. package/dist/source/services/plugin/plugin-registry.service.js +0 -215
  79. package/dist/source/services/plugin/plugin-ui-api.js +0 -46
  80. package/dist/source/services/plugin/plugin-updater.service.js +0 -206
  81. package/dist/source/services/scrobbling/scrobbling.service.js +0 -115
  82. package/dist/source/services/sleep-timer/sleep-timer.service.js +0 -45
  83. package/dist/source/services/version-check/version-check.service.js +0 -121
  84. package/dist/source/services/web/static-file.service.js +0 -185
  85. package/dist/source/services/web/web-server-manager.js +0 -507
  86. package/dist/source/services/web/web-streaming.service.js +0 -292
  87. package/dist/source/services/web/websocket.server.js +0 -267
  88. package/dist/source/services/youtube-music/api.js +0 -649
  89. package/dist/source/services/youtube-music/search.service.js +0 -38
  90. package/dist/source/stores/history.store.js +0 -64
  91. package/dist/source/stores/navigation.store.js +0 -90
  92. package/dist/source/stores/player.store.js +0 -789
  93. package/dist/source/stores/plugins.store.js +0 -177
  94. package/dist/source/types/actions.js +0 -1
  95. package/dist/source/types/cli.types.js +0 -1
  96. package/dist/source/types/config.types.js +0 -1
  97. package/dist/source/types/history.types.js +0 -1
  98. package/dist/source/types/import.types.js +0 -2
  99. package/dist/source/types/keyboard.types.js +0 -1
  100. package/dist/source/types/navigation.types.js +0 -1
  101. package/dist/source/types/player.types.js +0 -1
  102. package/dist/source/types/playlist.types.js +0 -1
  103. package/dist/source/types/plugin.types.js +0 -1
  104. package/dist/source/types/theme.types.js +0 -1
  105. package/dist/source/types/web.types.js +0 -2
  106. package/dist/source/types/youtube-music.types.js +0 -1
  107. package/dist/source/types/youtubei.types.js +0 -3
  108. package/dist/source/utils/constants.js +0 -135
  109. package/dist/source/utils/format.js +0 -24
  110. package/dist/source/utils/icons.js +0 -28
  111. 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();