@lucaperret/tidal-cli 1.1.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,8 +18,22 @@ const artist_1 = require("./artist");
18
18
  const track_1 = require("./track");
19
19
  const album_1 = require("./album");
20
20
  const recommend_1 = require("./recommend");
21
+ const mix_1 = require("./mix");
21
22
  const user_1 = require("./user");
22
23
  const history_1 = require("./history");
24
+ const search_history_1 = require("./search-history");
25
+ const saved_1 = require("./saved");
26
+ const share_1 = require("./share");
27
+ const MIX_CATEGORIES = ['daily', 'discovery', 'new-release', 'offline'];
28
+ function parseMixCategory(value) {
29
+ if (!value)
30
+ return undefined;
31
+ if (!MIX_CATEGORIES.includes(value)) {
32
+ console.error(`Error: invalid mix category "${value}". Use one of: ${MIX_CATEGORIES.join(', ')}`);
33
+ process.exit(2);
34
+ }
35
+ return value;
36
+ }
23
37
  const program = new commander_1.Command();
24
38
  program
25
39
  .name('tidal-cli')
@@ -85,6 +99,24 @@ searchCmd
85
99
  .action(wrapAction(async (genre) => {
86
100
  await (0, search_1.search)('playlist', genre || 'top hits', getJson());
87
101
  }));
102
+ searchCmd
103
+ .command('history')
104
+ .description('List your search history')
105
+ .action(wrapAction(async () => {
106
+ await (0, search_history_1.listSearchHistory)(getJson());
107
+ }));
108
+ searchCmd
109
+ .command('history-delete <entry-id>')
110
+ .description('Delete a single search history entry')
111
+ .action(wrapAction(async (entryId) => {
112
+ await (0, search_history_1.deleteSearchHistoryEntry)(entryId, getJson());
113
+ }));
114
+ searchCmd
115
+ .command('history-clear')
116
+ .description('Clear all search history entries')
117
+ .action(wrapAction(async () => {
118
+ await (0, search_history_1.clearSearchHistory)(getJson());
119
+ }));
88
120
  // Artist
89
121
  const artistCmd = program
90
122
  .command('artist')
@@ -167,8 +199,71 @@ albumCmd
167
199
  program
168
200
  .command('recommend')
169
201
  .description('Get personalized recommendations')
202
+ .option('--type <category>', `Mix category: ${MIX_CATEGORIES.join(', ')}`)
203
+ .action(wrapAction(async (opts) => {
204
+ await (0, recommend_1.getRecommendations)(parseMixCategory(opts.type), getJson());
205
+ }));
206
+ // Mix
207
+ const mixCmd = program
208
+ .command('mix')
209
+ .description('Browse personalized mix contents');
210
+ mixCmd
211
+ .command('items <mix-id>')
212
+ .description('Get items inside a specific mix')
213
+ .requiredOption('--type <category>', `Mix category: ${MIX_CATEGORIES.join(', ')}`)
214
+ .action(wrapAction(async (mixId, opts) => {
215
+ const cat = parseMixCategory(opts.type);
216
+ if (!cat)
217
+ return;
218
+ await (0, mix_1.getMixItems)(cat, mixId, getJson());
219
+ }));
220
+ // Share
221
+ program
222
+ .command('share <type> <id>')
223
+ .description('Create a share link for a track or album (type: track | album)')
224
+ .action(wrapAction(async (type, id) => {
225
+ const normalized = type === 'track' ? 'tracks' : type === 'album' ? 'albums' : null;
226
+ if (!normalized) {
227
+ console.error('Error: type must be "track" or "album"');
228
+ process.exit(2);
229
+ }
230
+ await (0, share_1.createShare)(normalized, id, getJson());
231
+ }));
232
+ // Saved (save for later)
233
+ const savedCmd = program
234
+ .command('saved')
235
+ .description('Manage your save-for-later collection');
236
+ savedCmd
237
+ .command('list')
238
+ .description('List items saved for later')
170
239
  .action(wrapAction(async () => {
171
- await (0, recommend_1.getRecommendations)(getJson());
240
+ await (0, saved_1.listSavedItems)(getJson());
241
+ }));
242
+ savedCmd
243
+ .command('add')
244
+ .description('Save an item for later')
245
+ .requiredOption('--type <type>', 'Item type: tracks, albums, artists, playlists, videos')
246
+ .requiredOption('--id <id>', 'Item ID')
247
+ .action(wrapAction(async (opts) => {
248
+ const allowed = ['tracks', 'albums', 'artists', 'playlists', 'videos'];
249
+ if (!allowed.includes(opts.type)) {
250
+ console.error(`Error: invalid type. Use one of: ${allowed.join(', ')}`);
251
+ process.exit(2);
252
+ }
253
+ await (0, saved_1.addSavedItem)(opts.type, opts.id, getJson());
254
+ }));
255
+ savedCmd
256
+ .command('remove')
257
+ .description('Remove an item from saved')
258
+ .requiredOption('--type <type>', 'Item type')
259
+ .requiredOption('--id <id>', 'Item ID')
260
+ .action(wrapAction(async (opts) => {
261
+ const allowed = ['tracks', 'albums', 'artists', 'playlists', 'videos'];
262
+ if (!allowed.includes(opts.type)) {
263
+ console.error(`Error: invalid type. Use one of: ${allowed.join(', ')}`);
264
+ process.exit(2);
265
+ }
266
+ await (0, saved_1.removeSavedItem)(opts.type, opts.id, getJson());
172
267
  }));
173
268
  // User
174
269
  const userCmd = program
package/dist/library.d.ts CHANGED
@@ -1,7 +1,30 @@
1
- type LibraryResourceType = 'artist' | 'album' | 'track' | 'video';
1
+ import type { LibraryResourceType } from './types';
2
+ export type { LibraryResourceType };
3
+ export declare function addToLibraryData(resourceType: LibraryResourceType, resourceId: string, client: any): Promise<{
4
+ resourceType: string;
5
+ resourceId: string;
6
+ added: boolean;
7
+ }>;
2
8
  export declare function addToLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
9
+ export declare function removeFromLibraryData(resourceType: LibraryResourceType, resourceId: string, client: any): Promise<{
10
+ resourceType: string;
11
+ resourceId: string;
12
+ removed: boolean;
13
+ }>;
3
14
  export declare function removeFromLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
15
+ export declare function listFavoritedPlaylistsData(client: any): Promise<Array<{
16
+ id: string;
17
+ name: string;
18
+ numberOfItems?: number;
19
+ }>>;
4
20
  export declare function listFavoritedPlaylists(json: boolean): Promise<void>;
21
+ export declare function addPlaylistToFavoritesData(playlistId: string, client: any): Promise<{
22
+ playlistId: string;
23
+ added: boolean;
24
+ }>;
5
25
  export declare function addPlaylistToFavorites(playlistId: string, json: boolean): Promise<void>;
26
+ export declare function removePlaylistFromFavoritesData(playlistId: string, client: any): Promise<{
27
+ playlistId: string;
28
+ removed: boolean;
29
+ }>;
6
30
  export declare function removePlaylistFromFavorites(playlistId: string, json: boolean): Promise<void>;
7
- export {};
package/dist/library.js CHANGED
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addToLibraryData = addToLibraryData;
3
4
  exports.addToLibrary = addToLibrary;
5
+ exports.removeFromLibraryData = removeFromLibraryData;
4
6
  exports.removeFromLibrary = removeFromLibrary;
7
+ exports.listFavoritedPlaylistsData = listFavoritedPlaylistsData;
5
8
  exports.listFavoritedPlaylists = listFavoritedPlaylists;
9
+ exports.addPlaylistToFavoritesData = addPlaylistToFavoritesData;
6
10
  exports.addPlaylistToFavorites = addPlaylistToFavorites;
11
+ exports.removePlaylistFromFavoritesData = removePlaylistFromFavoritesData;
7
12
  exports.removePlaylistFromFavorites = removePlaylistFromFavorites;
8
13
  const auth_1 = require("./auth");
9
14
  const collectionEndpoints = {
@@ -12,8 +17,7 @@ const collectionEndpoints = {
12
17
  track: { path: '/userCollectionTracks/{id}/relationships/items', type: 'tracks' },
13
18
  video: { path: '/userCollectionVideos/{id}/relationships/items', type: 'videos' },
14
19
  };
15
- async function addToLibrary(resourceType, resourceId, json) {
16
- const client = await (0, auth_1.getApiClient)();
20
+ async function addToLibraryData(resourceType, resourceId, client) {
17
21
  const endpoint = collectionEndpoints[resourceType];
18
22
  const { error } = await client.POST(endpoint.path, {
19
23
  params: { path: { id: 'me' } },
@@ -22,17 +26,26 @@ async function addToLibrary(resourceType, resourceId, json) {
22
26
  },
23
27
  });
24
28
  if (error) {
25
- console.error(`Error: Failed to add ${resourceType} to library — ${JSON.stringify(error)}`);
26
- process.exit(1);
29
+ throw new Error(`Failed to add ${resourceType} to library — ${JSON.stringify(error)}`);
27
30
  }
28
- if (json) {
29
- console.log(JSON.stringify({ resourceType, resourceId, added: true }, null, 2));
30
- return;
31
- }
32
- console.log(`\n${capitalize(resourceType)} ${resourceId} added to your library.`);
31
+ return { resourceType, resourceId, added: true };
33
32
  }
34
- async function removeFromLibrary(resourceType, resourceId, json) {
33
+ async function addToLibrary(resourceType, resourceId, json) {
35
34
  const client = await (0, auth_1.getApiClient)();
35
+ try {
36
+ const result = await addToLibraryData(resourceType, resourceId, client);
37
+ if (json) {
38
+ console.log(JSON.stringify(result, null, 2));
39
+ return;
40
+ }
41
+ console.log(`\n${capitalize(resourceType)} ${resourceId} added to your library.`);
42
+ }
43
+ catch (err) {
44
+ console.error(`Error: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ async function removeFromLibraryData(resourceType, resourceId, client) {
36
49
  const endpoint = collectionEndpoints[resourceType];
37
50
  const { error } = await client.DELETE(endpoint.path, {
38
51
  params: { path: { id: 'me' } },
@@ -41,17 +54,26 @@ async function removeFromLibrary(resourceType, resourceId, json) {
41
54
  },
42
55
  });
43
56
  if (error) {
44
- console.error(`Error: Failed to remove ${resourceType} from library — ${JSON.stringify(error)}`);
45
- process.exit(1);
46
- }
47
- if (json) {
48
- console.log(JSON.stringify({ resourceType, resourceId, removed: true }, null, 2));
49
- return;
57
+ throw new Error(`Failed to remove ${resourceType} from library — ${JSON.stringify(error)}`);
50
58
  }
51
- console.log(`\n${capitalize(resourceType)} ${resourceId} removed from your library.`);
59
+ return { resourceType, resourceId, removed: true };
52
60
  }
53
- async function listFavoritedPlaylists(json) {
61
+ async function removeFromLibrary(resourceType, resourceId, json) {
54
62
  const client = await (0, auth_1.getApiClient)();
63
+ try {
64
+ const result = await removeFromLibraryData(resourceType, resourceId, client);
65
+ if (json) {
66
+ console.log(JSON.stringify(result, null, 2));
67
+ return;
68
+ }
69
+ console.log(`\n${capitalize(resourceType)} ${resourceId} removed from your library.`);
70
+ }
71
+ catch (err) {
72
+ console.error(`Error: ${err.message}`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ async function listFavoritedPlaylistsData(client) {
55
77
  const { data, error } = await client.GET('/userCollectionPlaylists/{id}/relationships/items', {
56
78
  params: {
57
79
  path: { id: 'me' },
@@ -61,11 +83,10 @@ async function listFavoritedPlaylists(json) {
61
83
  },
62
84
  });
63
85
  if (error || !data) {
64
- console.error(`Error: Failed to list favorited playlists — ${JSON.stringify(error)}`);
65
- process.exit(1);
86
+ throw new Error(`Failed to list favorited playlists — ${JSON.stringify(error)}`);
66
87
  }
67
88
  const included = data.included ?? [];
68
- const playlists = included
89
+ return included
69
90
  .filter((item) => item.type === 'playlists')
70
91
  .map((item) => {
71
92
  const attrs = item.attributes;
@@ -75,22 +96,31 @@ async function listFavoritedPlaylists(json) {
75
96
  numberOfItems: attrs?.numberOfItems,
76
97
  };
77
98
  });
78
- if (json) {
79
- console.log(JSON.stringify(playlists, null, 2));
80
- return;
81
- }
82
- if (playlists.length === 0) {
83
- console.log('No favorited playlists found.');
84
- return;
99
+ }
100
+ async function listFavoritedPlaylists(json) {
101
+ const client = await (0, auth_1.getApiClient)();
102
+ try {
103
+ const playlists = await listFavoritedPlaylistsData(client);
104
+ if (json) {
105
+ console.log(JSON.stringify(playlists, null, 2));
106
+ return;
107
+ }
108
+ if (playlists.length === 0) {
109
+ console.log('No favorited playlists found.');
110
+ return;
111
+ }
112
+ console.log('\nFavorited playlists:\n');
113
+ for (const p of playlists) {
114
+ console.log(` [${p.id}] ${p.name} (${p.numberOfItems ?? 0} items)`);
115
+ }
116
+ console.log();
85
117
  }
86
- console.log('\nFavorited playlists:\n');
87
- for (const p of playlists) {
88
- console.log(` [${p.id}] ${p.name} (${p.numberOfItems ?? 0} items)`);
118
+ catch (err) {
119
+ console.error(`Error: ${err.message}`);
120
+ process.exit(1);
89
121
  }
90
- console.log();
91
122
  }
92
- async function addPlaylistToFavorites(playlistId, json) {
93
- const client = await (0, auth_1.getApiClient)();
123
+ async function addPlaylistToFavoritesData(playlistId, client) {
94
124
  const { error } = await client.POST('/userCollectionPlaylists/{id}/relationships/items', {
95
125
  params: { path: { id: 'me' } },
96
126
  body: {
@@ -98,17 +128,26 @@ async function addPlaylistToFavorites(playlistId, json) {
98
128
  },
99
129
  });
100
130
  if (error) {
101
- console.error(`Error: Failed to add playlist to favorites — ${JSON.stringify(error)}`);
102
- process.exit(1);
103
- }
104
- if (json) {
105
- console.log(JSON.stringify({ playlistId, added: true }, null, 2));
106
- return;
131
+ throw new Error(`Failed to add playlist to favorites — ${JSON.stringify(error)}`);
107
132
  }
108
- console.log(`\nPlaylist ${playlistId} added to favorites.`);
133
+ return { playlistId, added: true };
109
134
  }
110
- async function removePlaylistFromFavorites(playlistId, json) {
135
+ async function addPlaylistToFavorites(playlistId, json) {
111
136
  const client = await (0, auth_1.getApiClient)();
137
+ try {
138
+ const result = await addPlaylistToFavoritesData(playlistId, client);
139
+ if (json) {
140
+ console.log(JSON.stringify(result, null, 2));
141
+ return;
142
+ }
143
+ console.log(`\nPlaylist ${playlistId} added to favorites.`);
144
+ }
145
+ catch (err) {
146
+ console.error(`Error: ${err.message}`);
147
+ process.exit(1);
148
+ }
149
+ }
150
+ async function removePlaylistFromFavoritesData(playlistId, client) {
112
151
  const { error } = await client.DELETE('/userCollectionPlaylists/{id}/relationships/items', {
113
152
  params: { path: { id: 'me' } },
114
153
  body: {
@@ -116,14 +155,24 @@ async function removePlaylistFromFavorites(playlistId, json) {
116
155
  },
117
156
  });
118
157
  if (error) {
119
- console.error(`Error: Failed to remove playlist from favorites — ${JSON.stringify(error)}`);
120
- process.exit(1);
158
+ throw new Error(`Failed to remove playlist from favorites — ${JSON.stringify(error)}`);
121
159
  }
122
- if (json) {
123
- console.log(JSON.stringify({ playlistId, removed: true }, null, 2));
124
- return;
160
+ return { playlistId, removed: true };
161
+ }
162
+ async function removePlaylistFromFavorites(playlistId, json) {
163
+ const client = await (0, auth_1.getApiClient)();
164
+ try {
165
+ const result = await removePlaylistFromFavoritesData(playlistId, client);
166
+ if (json) {
167
+ console.log(JSON.stringify(result, null, 2));
168
+ return;
169
+ }
170
+ console.log(`\nPlaylist ${playlistId} removed from favorites.`);
171
+ }
172
+ catch (err) {
173
+ console.error(`Error: ${err.message}`);
174
+ process.exit(1);
125
175
  }
126
- console.log(`\nPlaylist ${playlistId} removed from favorites.`);
127
176
  }
128
177
  function capitalize(s) {
129
178
  return s.charAt(0).toUpperCase() + s.slice(1);
package/dist/mix.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { MixCategory, MixItem } from './types';
2
+ export type { MixItem };
3
+ export declare function getMixItemsData(category: MixCategory, mixId: string, client: any, countryCode: string): Promise<MixItem[]>;
4
+ export declare function getMixItems(category: MixCategory, mixId: string, json: boolean): Promise<void>;
package/dist/mix.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMixItemsData = getMixItemsData;
4
+ exports.getMixItems = getMixItems;
5
+ const auth_1 = require("./auth");
6
+ const categoryEndpoints = {
7
+ daily: '/userDailyMixes/{id}/relationships/items',
8
+ discovery: '/userDiscoveryMixes/{id}/relationships/items',
9
+ 'new-release': '/userNewReleaseMixes/{id}/relationships/items',
10
+ offline: '/userOfflineMixes/{id}/relationships/items',
11
+ };
12
+ async function getMixItemsData(category, mixId, client, countryCode) {
13
+ const { data, error } = await client.GET(categoryEndpoints[category], {
14
+ params: {
15
+ path: { id: mixId },
16
+ query: {
17
+ countryCode,
18
+ include: ['items'],
19
+ },
20
+ },
21
+ });
22
+ if (error || !data) {
23
+ throw new Error(`Failed to get mix items — ${JSON.stringify(error)}`);
24
+ }
25
+ const included = data.included ?? [];
26
+ return included.map((item) => {
27
+ const attrs = item.attributes ?? {};
28
+ return {
29
+ id: item.id,
30
+ type: item.type,
31
+ name: attrs.title ?? attrs.name ?? 'Untitled',
32
+ };
33
+ });
34
+ }
35
+ async function getMixItems(category, mixId, json) {
36
+ const client = await (0, auth_1.getApiClient)();
37
+ const countryCode = await (0, auth_1.getCountryCode)();
38
+ try {
39
+ const items = await getMixItemsData(category, mixId, client, countryCode);
40
+ if (json) {
41
+ console.log(JSON.stringify(items, null, 2));
42
+ return;
43
+ }
44
+ if (items.length === 0) {
45
+ console.log('No items found in this mix.');
46
+ return;
47
+ }
48
+ console.log(`\n${category} mix [${mixId}] items:\n`);
49
+ for (const item of items) {
50
+ console.log(` [${item.id}] (${item.type}) ${item.name}`);
51
+ }
52
+ console.log();
53
+ }
54
+ catch (err) {
55
+ console.error(`Error: ${err.message}`);
56
+ process.exit(1);
57
+ }
58
+ }
59
+ //# sourceMappingURL=mix.js.map
@@ -1,3 +1,7 @@
1
+ import type { PlaybackInfo, PlaybackUrl } from './types';
2
+ export type { PlaybackInfo, PlaybackUrl };
3
+ export declare function playbackInfoData(trackId: string, quality: string, client: any): Promise<PlaybackInfo>;
1
4
  export declare function playbackInfo(trackId: string, quality: string, json: boolean): Promise<void>;
5
+ export declare function playbackUrlData(trackId: string, quality: string, client: any): Promise<PlaybackUrl>;
2
6
  export declare function playbackUrl(trackId: string, quality: string, json: boolean): Promise<void>;
3
7
  export declare function playbackPlay(trackId: string, quality: string): Promise<void>;
package/dist/playback.js CHANGED
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.playbackInfoData = playbackInfoData;
36
37
  exports.playbackInfo = playbackInfo;
38
+ exports.playbackUrlData = playbackUrlData;
37
39
  exports.playbackUrl = playbackUrl;
38
40
  exports.playbackPlay = playbackPlay;
39
41
  const auth_1 = require("./auth");
@@ -62,8 +64,7 @@ function parseDataUri(dataUri) {
62
64
  throw new Error('Invalid data URI from trackManifests');
63
65
  return { mimeType: match[1], data: match[2] };
64
66
  }
65
- async function fetchTrackManifest(trackId, quality) {
66
- const client = await (0, auth_1.getApiClient)();
67
+ async function fetchTrackManifestData(trackId, quality, client) {
67
68
  const formats = qualityToFormats[quality] ?? qualityToFormats.HIGH;
68
69
  const { data, error } = await client.GET('/trackManifests/{id}', {
69
70
  params: {
@@ -78,13 +79,11 @@ async function fetchTrackManifest(trackId, quality) {
78
79
  },
79
80
  });
80
81
  if (error || !data) {
81
- console.error(`Error: Failed to get track manifest — ${JSON.stringify(error)}`);
82
- process.exit(1);
82
+ throw new Error(`Failed to get track manifest — ${JSON.stringify(error)}`);
83
83
  }
84
84
  const attrs = data.data?.attributes;
85
85
  if (!attrs?.uri) {
86
- console.error('Error: No manifest URI in response.');
87
- process.exit(1);
86
+ throw new Error('No manifest URI in response.');
88
87
  }
89
88
  const { mimeType, data: manifestBase64 } = parseDataUri(attrs.uri);
90
89
  return {
@@ -101,6 +100,11 @@ async function fetchTrackManifest(trackId, quality) {
101
100
  albumPeakAmplitude: attrs.albumAudioNormalizationData?.peakAmplitude ?? 0,
102
101
  };
103
102
  }
103
+ // Keep backward-compatible internal helper
104
+ async function fetchTrackManifest(trackId, quality) {
105
+ const client = await (0, auth_1.getApiClient)();
106
+ return fetchTrackManifestData(trackId, quality, client);
107
+ }
104
108
  function decodeManifest(base64Manifest, mimeType) {
105
109
  const decoded = Buffer.from(base64Manifest, 'base64').toString('utf-8');
106
110
  // JSON manifest (BTS): has urls array
@@ -118,7 +122,6 @@ function decodeManifest(base64Manifest, mimeType) {
118
122
  const initMatch = decoded.match(/initialization="([^"]+)"/);
119
123
  const mediaMatch = decoded.match(/media="([^"]+)"/);
120
124
  const codecsMatch = decoded.match(/codecs="([^"]+)"/);
121
- // Parse segment timeline: <S d="176128" r="6"/> means 7 segments, <S d="89088"/> means 1
122
125
  const segmentDurations = [];
123
126
  const sMatches = decoded.matchAll(/<S d="(\d+)"(?:\s+r="(\d+)")?\/>/g);
124
127
  let segNum = 1;
@@ -139,7 +142,6 @@ function decodeManifest(base64Manifest, mimeType) {
139
142
  codecs: codecsMatch?.[1],
140
143
  };
141
144
  }
142
- // Fallback: try BaseURL
143
145
  const baseUrlMatch = decoded.match(/<BaseURL>([^<]+)<\/BaseURL>/);
144
146
  if (baseUrlMatch) {
145
147
  return { type: 'direct', url: baseUrlMatch[1], codecs: codecsMatch?.[1] };
@@ -148,12 +150,10 @@ function decodeManifest(base64Manifest, mimeType) {
148
150
  throw new Error('Unable to parse manifest');
149
151
  }
150
152
  async function downloadDashStream(dash) {
151
- // Download init segment
152
153
  const initRes = await fetch(dash.initUrl);
153
154
  if (!initRes.ok)
154
155
  throw new Error(`Failed to download init segment (${initRes.status})`);
155
156
  const initBuf = Buffer.from(await initRes.arrayBuffer());
156
- // Download media segments
157
157
  const segBuffers = [initBuf];
158
158
  for (const segNum of dash.segments) {
159
159
  const segUrl = dash.mediaTemplate.replace('$Number$', String(segNum));
@@ -164,9 +164,9 @@ async function downloadDashStream(dash) {
164
164
  }
165
165
  return Buffer.concat(segBuffers);
166
166
  }
167
- async function playbackInfo(trackId, quality, json) {
168
- const info = await fetchTrackManifest(trackId, quality);
169
- const result = {
167
+ async function playbackInfoData(trackId, quality, client) {
168
+ const info = await fetchTrackManifestData(trackId, quality, client);
169
+ return {
170
170
  trackId: info.trackId,
171
171
  presentation: info.trackPresentation,
172
172
  previewReason: info.previewReason,
@@ -178,57 +178,73 @@ async function playbackInfo(trackId, quality, json) {
178
178
  albumReplayGain: info.albumReplayGain,
179
179
  albumPeakAmplitude: info.albumPeakAmplitude,
180
180
  };
181
- if (json) {
182
- console.log(JSON.stringify(result, null, 2));
183
- return;
181
+ }
182
+ async function playbackInfo(trackId, quality, json) {
183
+ const client = await (0, auth_1.getApiClient)();
184
+ try {
185
+ const result = await playbackInfoData(trackId, quality, client);
186
+ if (json) {
187
+ console.log(JSON.stringify(result, null, 2));
188
+ return;
189
+ }
190
+ console.log(`\nPlayback info for track ${trackId}:\n`);
191
+ console.log(` Quality: ${result.audioQuality}`);
192
+ console.log(` Formats: ${result.formats?.join(', ')}`);
193
+ console.log(` Presentation: ${result.presentation}`);
194
+ if (result.previewReason) {
195
+ console.log(` Preview reason: ${result.previewReason}`);
196
+ }
197
+ console.log(` Manifest type: ${result.manifestMimeType}`);
198
+ console.log(` Track gain: ${result.trackReplayGain} dB`);
199
+ console.log(` Album gain: ${result.albumReplayGain} dB`);
200
+ console.log();
184
201
  }
185
- console.log(`\nPlayback info for track ${trackId}:\n`);
186
- console.log(` Quality: ${result.audioQuality}`);
187
- console.log(` Formats: ${result.formats.join(', ')}`);
188
- console.log(` Presentation: ${result.presentation}`);
189
- if (result.previewReason) {
190
- console.log(` Preview reason: ${result.previewReason}`);
202
+ catch (err) {
203
+ console.error(`Error: ${err.message}`);
204
+ process.exit(1);
191
205
  }
192
- console.log(` Manifest type: ${result.manifestMimeType}`);
193
- console.log(` Track gain: ${result.trackReplayGain} dB`);
194
- console.log(` Album gain: ${result.albumReplayGain} dB`);
195
- console.log();
196
206
  }
197
- async function playbackUrl(trackId, quality, json) {
198
- const info = await fetchTrackManifest(trackId, quality);
207
+ async function playbackUrlData(trackId, quality, client) {
208
+ const info = await fetchTrackManifestData(trackId, quality, client);
199
209
  const stream = decodeManifest(info.manifest, info.manifestMimeType);
200
210
  if (stream.type === 'direct') {
201
- if (json) {
202
- console.log(JSON.stringify({ trackId: info.trackId, url: stream.url, audioQuality: info.audioQuality }, null, 2));
203
- }
204
- else {
205
- console.log(stream.url);
206
- }
211
+ return { trackId: info.trackId, url: stream.url, audioQuality: info.audioQuality, type: 'direct' };
207
212
  }
208
213
  else if (stream.dash) {
214
+ return {
215
+ trackId: info.trackId,
216
+ type: 'dash',
217
+ initUrl: stream.dash.initUrl,
218
+ segmentCount: stream.dash.segments.length,
219
+ audioQuality: info.audioQuality,
220
+ };
221
+ }
222
+ throw new Error('No stream URL available for this track.');
223
+ }
224
+ async function playbackUrl(trackId, quality, json) {
225
+ const client = await (0, auth_1.getApiClient)();
226
+ try {
227
+ const result = await playbackUrlData(trackId, quality, client);
209
228
  if (json) {
210
- console.log(JSON.stringify({
211
- trackId: info.trackId,
212
- type: 'dash',
213
- initUrl: stream.dash.initUrl,
214
- segmentCount: stream.dash.segments.length,
215
- audioQuality: info.audioQuality,
216
- }, null, 2));
229
+ console.log(JSON.stringify(result, null, 2));
230
+ return;
231
+ }
232
+ if (result.type === 'direct') {
233
+ console.log(result.url);
217
234
  }
218
235
  else {
219
- console.log(`DASH stream (${stream.dash.segments.length} segments)`);
220
- console.log(` Init: ${stream.dash.initUrl}`);
236
+ console.log(`DASH stream (${result.segmentCount} segments)`);
237
+ console.log(` Init: ${result.initUrl}`);
221
238
  }
222
239
  }
223
- else {
224
- console.error('Error: No stream URL available for this track.');
240
+ catch (err) {
241
+ console.error(`Error: ${err.message}`);
225
242
  process.exit(1);
226
243
  }
227
244
  }
228
245
  async function playbackPlay(trackId, quality) {
229
246
  const info = await fetchTrackManifest(trackId, quality);
230
247
  const stream = decodeManifest(info.manifest, info.manifestMimeType);
231
- // Determine file extension from format
232
248
  const isFlac = info.formats.includes('FLAC') || info.formats.includes('FLAC_HIRES');
233
249
  const ext = isFlac ? '.flac' : '.mp4';
234
250
  const tmpFile = path.join(os.tmpdir(), `tidal-${trackId}${ext}`);