ff1-cli 1.0.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.
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.confirmPlaylistForSending = confirmPlaylistForSending;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const fs_1 = require("fs");
42
+ /**
43
+ * Get available FF1 devices from config
44
+ *
45
+ * @returns {Promise<Array>} Array of device objects
46
+ */
47
+ async function getAvailableDevices() {
48
+ try {
49
+ const configModule = (await Promise.resolve().then(() => __importStar(require('../config'))));
50
+ const getFF1DeviceConfig = configModule.getFF1DeviceConfig ||
51
+ (configModule.default && configModule.default.getFF1DeviceConfig) ||
52
+ configModule.default;
53
+ if (typeof getFF1DeviceConfig !== 'function') {
54
+ return [];
55
+ }
56
+ const deviceConfig = getFF1DeviceConfig();
57
+ if (deviceConfig.devices && Array.isArray(deviceConfig.devices)) {
58
+ return deviceConfig.devices
59
+ .filter((d) => d && d.host)
60
+ .map((d) => ({
61
+ name: d.name || d.host,
62
+ host: d.host,
63
+ }));
64
+ }
65
+ }
66
+ catch (error) {
67
+ if (process.env.DEBUG) {
68
+ console.log(chalk_1.default.gray(`[DEBUG] Error loading devices: ${error.message}`));
69
+ }
70
+ // Silently fail if config can't be loaded
71
+ }
72
+ return [];
73
+ }
74
+ /**
75
+ * Confirm playlist file path and validate the playlist
76
+ *
77
+ * Reads the playlist file, validates it against DP-1 spec,
78
+ * and returns confirmation result for user review.
79
+ *
80
+ * @param {string} filePath - Path to playlist file
81
+ * @param {string} [deviceName] - Device name (optional)
82
+ * @returns {Promise<PlaylistSendConfirmation>} Validation result
83
+ */
84
+ async function confirmPlaylistForSending(filePath, deviceName) {
85
+ const defaultPath = './playlist.json';
86
+ const resolvedPath = filePath || defaultPath;
87
+ // Convert string "null" to undefined (in case model passes it literally)
88
+ const actualDeviceName = deviceName === 'null' || deviceName === '' ? undefined : deviceName;
89
+ if (process.env.DEBUG) {
90
+ console.error(chalk_1.default.gray(`[DEBUG] confirmPlaylistForSending called with: filePath="${filePath}", deviceName="${deviceName}" -> "${actualDeviceName}"`));
91
+ }
92
+ try {
93
+ // Check if file exists
94
+ console.log(chalk_1.default.cyan(`Checking playlist file: ${resolvedPath}...`));
95
+ let _fileExists = false;
96
+ let playlist;
97
+ try {
98
+ const content = await fs_1.promises.readFile(resolvedPath, 'utf-8');
99
+ playlist = JSON.parse(content);
100
+ _fileExists = true;
101
+ console.log(chalk_1.default.green('✓ File found'));
102
+ }
103
+ catch (error) {
104
+ const errorMsg = error.message;
105
+ if (errorMsg.includes('ENOENT') || errorMsg.includes('no such file')) {
106
+ console.log(chalk_1.default.red(`✗ File not found: ${resolvedPath}`));
107
+ return {
108
+ success: false,
109
+ filePath: resolvedPath,
110
+ fileExists: false,
111
+ playlistValid: false,
112
+ error: `Playlist file not found at ${resolvedPath}`,
113
+ message: `Could not find playlist file. Try:\n • Run a playlist build first\n • Check the file path is correct\n • Use "send ./path/to/playlist.json"`,
114
+ };
115
+ }
116
+ throw error;
117
+ }
118
+ if (!playlist) {
119
+ return {
120
+ success: false,
121
+ filePath: resolvedPath,
122
+ fileExists: true,
123
+ playlistValid: false,
124
+ error: 'Playlist file is empty',
125
+ };
126
+ }
127
+ // Validate playlist structure
128
+ console.log(chalk_1.default.cyan('Validating playlist...'));
129
+ // Dynamic import to avoid circular dependency
130
+ const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
131
+ const verifyResult = verifyPlaylist(playlist);
132
+ if (!verifyResult.valid) {
133
+ console.log(chalk_1.default.red('✗ Playlist validation failed'));
134
+ const detailLines = verifyResult.details?.map((d) => ` • ${d.path}: ${d.message}`).join('\n') ||
135
+ verifyResult.error;
136
+ const detailPaths = verifyResult.details?.map((d) => d.path) || [];
137
+ const hints = [];
138
+ if (detailPaths.some((path) => path.includes('signature'))) {
139
+ hints.push('Add `playlist.privateKey` (or `PLAYLIST_PRIVATE_KEY`) and rebuild the playlist to include a signature.');
140
+ }
141
+ if (detailPaths.some((path) => path.includes('defaults.display.margin'))) {
142
+ hints.push('Rebuild the playlist with the latest CLI defaults (margin must be numeric).');
143
+ }
144
+ const hintText = hints.length > 0 ? `\n\nHint:\n${hints.map((h) => ` • ${h}`).join('\n')}` : '';
145
+ return {
146
+ success: false,
147
+ filePath: resolvedPath,
148
+ fileExists: true,
149
+ playlistValid: false,
150
+ playlist,
151
+ deviceName: actualDeviceName,
152
+ error: `Playlist is invalid: ${verifyResult.error}`,
153
+ message: `This playlist doesn't match DP-1 specification.\n\nErrors:\n${detailLines}${hintText}`,
154
+ };
155
+ }
156
+ console.log(chalk_1.default.green('✓ Playlist is valid'));
157
+ // Display confirmation details
158
+ const itemCount = playlist.items?.length || 0;
159
+ const title = playlist.title || 'Untitled';
160
+ // Handle device selection
161
+ let selectedDevice = actualDeviceName;
162
+ let needsDeviceSelection = false;
163
+ let availableDevices = [];
164
+ if (!selectedDevice) {
165
+ // Get available devices
166
+ availableDevices = await getAvailableDevices();
167
+ if (process.env.DEBUG) {
168
+ console.error(chalk_1.default.gray(`[DEBUG] selectedDevice is null/undefined`));
169
+ console.error(chalk_1.default.gray(`[DEBUG] Available devices found: ${availableDevices.length}`));
170
+ availableDevices.forEach((d) => {
171
+ console.error(chalk_1.default.gray(`[DEBUG] Device: ${d.name} (${d.host})`));
172
+ });
173
+ }
174
+ if (availableDevices.length === 0) {
175
+ return {
176
+ success: false,
177
+ filePath: resolvedPath,
178
+ fileExists: true,
179
+ playlistValid: true,
180
+ playlist,
181
+ error: 'No FF1 devices configured',
182
+ message: `No FF1 devices found in your configuration.\n\nPlease add devices to your config.json:\n{\n "devices": [{\n "name": "Living Room",\n "host": "192.168.1.100"\n }]\n}`,
183
+ };
184
+ }
185
+ else if (availableDevices.length === 1) {
186
+ // Auto-select single device
187
+ selectedDevice = availableDevices[0].name || availableDevices[0].host;
188
+ console.log(chalk_1.default.cyan(`Auto-selecting device: ${selectedDevice}`));
189
+ }
190
+ else {
191
+ // Multiple devices - need user to choose
192
+ needsDeviceSelection = true;
193
+ }
194
+ }
195
+ console.log();
196
+ console.log(chalk_1.default.bold('Playlist Summary:'));
197
+ console.log(chalk_1.default.gray(` Title: ${title}`));
198
+ console.log(chalk_1.default.gray(` Items: ${itemCount}`));
199
+ if (selectedDevice) {
200
+ console.log(chalk_1.default.gray(` Device: ${selectedDevice}`));
201
+ }
202
+ else if (availableDevices.length > 1) {
203
+ console.log(chalk_1.default.gray(` Device: (to be selected)`));
204
+ }
205
+ console.log();
206
+ // If multiple devices, return needsDeviceSelection flag
207
+ if (needsDeviceSelection) {
208
+ return {
209
+ success: false,
210
+ filePath: resolvedPath,
211
+ fileExists: true,
212
+ playlistValid: true,
213
+ playlist,
214
+ needsDeviceSelection: true,
215
+ availableDevices,
216
+ error: 'Multiple devices available - please choose one',
217
+ message: `Which device would you like to display on?\n\nAvailable devices:\n${availableDevices.map((d, i) => ` ${i + 1}. ${d.name || d.host}`).join('\n')}\n\nSay: "send to [device name]" or "send to device 1"`,
218
+ };
219
+ }
220
+ return {
221
+ success: true,
222
+ filePath: resolvedPath,
223
+ fileExists: true,
224
+ playlistValid: true,
225
+ playlist,
226
+ deviceName: selectedDevice,
227
+ message: `Ready to send "${title}" (${itemCount} items) to ${selectedDevice}!`,
228
+ };
229
+ }
230
+ catch (error) {
231
+ const errorMsg = error.message;
232
+ console.log(chalk_1.default.red(`✗ Error: ${errorMsg}`));
233
+ return {
234
+ success: false,
235
+ filePath: resolvedPath,
236
+ fileExists: false,
237
+ playlistValid: false,
238
+ error: errorMsg,
239
+ };
240
+ }
241
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Playlist Signing Utility
3
+ * Uses dp1-js library for DP-1 specification-compliant playlist signing
4
+ */
5
+ const { signDP1Playlist, verifyPlaylistSignature } = require('dp1-js');
6
+ const { getPlaylistConfig } = require('../config');
7
+ /**
8
+ * Convert base64-encoded key to Uint8Array (or hex string if needed)
9
+ *
10
+ * @param {string} base64Key - Ed25519 private key in base64 format
11
+ * @returns {Uint8Array} Private key as Uint8Array
12
+ */
13
+ function base64ToUint8Array(base64Key) {
14
+ const buffer = Buffer.from(base64Key, 'base64');
15
+ return new Uint8Array(buffer);
16
+ }
17
+ /**
18
+ * Convert hex string to Uint8Array
19
+ *
20
+ * @param {string} hexKey - Ed25519 public key in hex format
21
+ * @returns {Uint8Array} Public key as Uint8Array
22
+ */
23
+ function hexToUint8Array(hexKey) {
24
+ const cleanHex = hexKey.replace(/^0x/, '');
25
+ const buffer = Buffer.from(cleanHex, 'hex');
26
+ return new Uint8Array(buffer);
27
+ }
28
+ /**
29
+ * Check if a string is valid hex (with or without 0x prefix)
30
+ *
31
+ * @param {string} str - String to test
32
+ * @returns {boolean} True if string is valid hex
33
+ */
34
+ function isHexString(str) {
35
+ const cleanHex = str.replace(/^0x/, '');
36
+ return /^[0-9a-fA-F]+$/.test(cleanHex) && cleanHex.length % 2 === 0;
37
+ }
38
+ /**
39
+ * Sign a playlist using ed25519 as per DP-1 specification
40
+ * Uses dp1-js library for standards-compliant signing
41
+ * Accepts private key in hex (with or without 0x prefix) or base64 format
42
+ *
43
+ * @param {Object} playlist - Playlist object without signature
44
+ * @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
45
+ * @returns {Promise<string>} Signature in format "ed25519:0x{hex}"
46
+ * @throws {Error} If private key is invalid or signing fails
47
+ * @example
48
+ * const signature = await signPlaylist(playlist, privateKeyHexOrBase64);
49
+ * // Returns: "ed25519:0x1234abcd..."
50
+ */
51
+ async function signPlaylist(playlist, privateKeyBase64) {
52
+ // Get private key from config if not provided
53
+ let privateKey = privateKeyBase64;
54
+ if (!privateKey) {
55
+ const config = getPlaylistConfig();
56
+ privateKey = config.privateKey;
57
+ }
58
+ if (!privateKey) {
59
+ throw new Error('Private key is required for signing');
60
+ }
61
+ try {
62
+ // Remove signature field if it exists (for re-signing)
63
+ const playlistToSign = { ...playlist };
64
+ delete playlistToSign.signature;
65
+ // Try hex first (with or without 0x prefix), then fall back to base64
66
+ let keyInput;
67
+ if (isHexString(privateKey)) {
68
+ // It's hex - ensure it has 0x prefix for dp1-js
69
+ keyInput = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey;
70
+ }
71
+ else {
72
+ // Fall back to base64
73
+ const keyBytes = base64ToUint8Array(privateKey);
74
+ keyInput = '0x' + Buffer.from(keyBytes).toString('hex');
75
+ }
76
+ // Sign using dp1-js library
77
+ const signature = await signDP1Playlist(playlistToSign, keyInput);
78
+ return signature;
79
+ }
80
+ catch (error) {
81
+ throw new Error(`Failed to sign playlist: ${error.message}`);
82
+ }
83
+ }
84
+ /**
85
+ * Verify a playlist's ed25519 signature
86
+ *
87
+ * @param {Object} playlist - Playlist object with signature field
88
+ * @param {string} publicKeyHex - Ed25519 public key in hex format (with or without 0x prefix)
89
+ * @returns {Promise<boolean>} True if signature is valid, false otherwise
90
+ * @throws {Error} If verification process fails
91
+ * @example
92
+ * const isValid = await verifyPlaylist(signedPlaylist, publicKeyHex);
93
+ * if (isValid) {
94
+ * console.log('Signature is valid');
95
+ * }
96
+ */
97
+ async function verifyPlaylist(playlist, publicKeyHex) {
98
+ if (!playlist.signature) {
99
+ throw new Error('Playlist does not have a signature');
100
+ }
101
+ if (!publicKeyHex) {
102
+ throw new Error('Public key is required for verification');
103
+ }
104
+ try {
105
+ // Convert hex public key to Uint8Array
106
+ const publicKeyBytes = hexToUint8Array(publicKeyHex);
107
+ // Verify using dp1-js library
108
+ const isValid = await verifyPlaylistSignature(playlist, publicKeyBytes);
109
+ return isValid;
110
+ }
111
+ catch (error) {
112
+ throw new Error(`Failed to verify playlist signature: ${error.message}`);
113
+ }
114
+ }
115
+ /**
116
+ * Sign a playlist file
117
+ * Reads playlist from file, signs it, and writes back
118
+ *
119
+ * @param {string} playlistPath - Path to playlist JSON file
120
+ * @param {string} [privateKeyBase64] - Ed25519 private key in hex or base64 format (optional, uses config if not provided)
121
+ * @param {string} [outputPath] - Output path (optional, overwrites input if not provided)
122
+ * @returns {Promise<Object>} Result with signed playlist
123
+ * @returns {boolean} returns.success - Whether signing succeeded
124
+ * @returns {Object} [returns.playlist] - Signed playlist object
125
+ * @returns {string} [returns.error] - Error message if failed
126
+ * @example
127
+ * const result = await signPlaylistFile('playlist.json');
128
+ * if (result.success) {
129
+ * console.log('Playlist signed:', result.playlist);
130
+ * }
131
+ */
132
+ async function signPlaylistFile(playlistPath, privateKeyBase64, outputPath) {
133
+ const fs = require('fs');
134
+ const path = require('path');
135
+ try {
136
+ // Read playlist file
137
+ if (!fs.existsSync(playlistPath)) {
138
+ throw new Error(`Playlist file not found: ${playlistPath}`);
139
+ }
140
+ const playlistContent = fs.readFileSync(playlistPath, 'utf-8');
141
+ const playlist = JSON.parse(playlistContent);
142
+ // Sign playlist
143
+ const signature = await signPlaylist(playlist, privateKeyBase64);
144
+ // Add signature to playlist
145
+ const signedPlaylist = {
146
+ ...playlist,
147
+ signature,
148
+ };
149
+ // Write to output file
150
+ const output = outputPath || playlistPath;
151
+ fs.writeFileSync(output, JSON.stringify(signedPlaylist, null, 2), 'utf-8');
152
+ console.log(`✓ Playlist signed and saved to: ${path.resolve(output)}`);
153
+ return {
154
+ success: true,
155
+ playlist: signedPlaylist,
156
+ };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ success: false,
161
+ error: error.message,
162
+ };
163
+ }
164
+ }
165
+ module.exports = {
166
+ signPlaylist,
167
+ verifyPlaylist,
168
+ signPlaylistFile,
169
+ base64ToUint8Array,
170
+ hexToUint8Array,
171
+ };
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * Playlist Verification Utility
4
+ * Uses dp1-js library for DP-1 specification-compliant playlist validation
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.verifyPlaylist = verifyPlaylist;
11
+ exports.verifyPlaylistFile = verifyPlaylistFile;
12
+ exports.printVerificationResult = printVerificationResult;
13
+ const dp1_js_1 = require("dp1-js");
14
+ const chalk_1 = __importDefault(require("chalk"));
15
+ const fs_1 = require("fs");
16
+ /**
17
+ * Verify playlist structure and integrity
18
+ *
19
+ * Validates playlist against DP-1 specification using dp1-js parser.
20
+ * Returns detailed validation errors if playlist is invalid.
21
+ *
22
+ * @param {Object} playlist - Playlist object to verify
23
+ * @returns {Object} Verification result
24
+ * @returns {boolean} returns.valid - Whether playlist is valid
25
+ * @returns {string} [returns.error] - Error message if invalid
26
+ * @returns {Array<Object>} [returns.details] - Detailed validation errors
27
+ * @example
28
+ * const result = verifyPlaylist(playlist);
29
+ * if (result.valid) {
30
+ * console.log('Playlist is valid');
31
+ * } else {
32
+ * console.error('Invalid:', result.error);
33
+ * }
34
+ */
35
+ function verifyPlaylist(playlist) {
36
+ try {
37
+ // Use dp1-js parseDP1Playlist for validation
38
+ const result = (0, dp1_js_1.parseDP1Playlist)(playlist);
39
+ if (result.error) {
40
+ return {
41
+ valid: false,
42
+ error: result.error.message,
43
+ details: result.error.details || [],
44
+ };
45
+ }
46
+ // Validation successful
47
+ return {
48
+ valid: true,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return {
53
+ valid: false,
54
+ error: `Verification failed: ${error.message}`,
55
+ };
56
+ }
57
+ }
58
+ /**
59
+ * Verify playlist file
60
+ *
61
+ * Reads playlist from file and validates structure.
62
+ *
63
+ * @param {string} playlistPath - Path to playlist JSON file
64
+ * @returns {Promise<Object>} Verification result
65
+ * @returns {boolean} returns.valid - Whether playlist is valid
66
+ * @returns {Object} [returns.playlist] - Validated playlist object
67
+ * @returns {string} [returns.error] - Error message if invalid
68
+ * @returns {Array<Object>} [returns.details] - Detailed validation errors
69
+ * @example
70
+ * const result = await verifyPlaylistFile('playlist.json');
71
+ * if (result.valid) {
72
+ * console.log('Playlist is valid');
73
+ * }
74
+ */
75
+ async function verifyPlaylistFile(playlistPath) {
76
+ try {
77
+ // Check if file exists
78
+ try {
79
+ await fs_1.promises.access(playlistPath);
80
+ }
81
+ catch {
82
+ return {
83
+ valid: false,
84
+ error: `Playlist file not found: ${playlistPath}`,
85
+ };
86
+ }
87
+ // Read and parse playlist file
88
+ const playlistContent = await fs_1.promises.readFile(playlistPath, 'utf-8');
89
+ let playlistData;
90
+ try {
91
+ playlistData = JSON.parse(playlistContent);
92
+ }
93
+ catch (parseError) {
94
+ return {
95
+ valid: false,
96
+ error: `Invalid JSON: ${parseError.message}`,
97
+ };
98
+ }
99
+ // Verify using dp1-js
100
+ const result = verifyPlaylist(playlistData);
101
+ if (result.valid) {
102
+ return {
103
+ valid: true,
104
+ playlist: playlistData,
105
+ };
106
+ }
107
+ return {
108
+ valid: false,
109
+ error: result.error,
110
+ details: result.details,
111
+ };
112
+ }
113
+ catch (error) {
114
+ return {
115
+ valid: false,
116
+ error: `Failed to verify playlist file: ${error.message}`,
117
+ };
118
+ }
119
+ }
120
+ /**
121
+ * Print verification results to console
122
+ *
123
+ * @param {Object} result - Verification result
124
+ * @param {string} [filename] - Optional filename to include in output
125
+ */
126
+ function printVerificationResult(result, filename) {
127
+ if (result.valid) {
128
+ console.log(chalk_1.default.green('\n✅ Playlist is valid!'));
129
+ if (filename) {
130
+ console.log(chalk_1.default.gray(` File: ${filename}`));
131
+ }
132
+ if (result.playlist) {
133
+ console.log(chalk_1.default.gray(` Title: ${result.playlist.title}`));
134
+ console.log(chalk_1.default.gray(` Items: ${result.playlist.items?.length || 0}`));
135
+ console.log(chalk_1.default.gray(` DP Version: ${result.playlist.dpVersion}`));
136
+ if (result.playlist.signature && typeof result.playlist.signature === 'string') {
137
+ console.log(chalk_1.default.gray(` Signature: ${result.playlist.signature.substring(0, 30)}...`));
138
+ }
139
+ }
140
+ console.log();
141
+ }
142
+ else {
143
+ console.log(chalk_1.default.red('\n❌ Playlist validation failed!'));
144
+ if (filename) {
145
+ console.log(chalk_1.default.gray(` File: ${filename}`));
146
+ }
147
+ console.log(chalk_1.default.red(` Error: ${result.error}`));
148
+ if (result.details && result.details.length > 0) {
149
+ console.log(chalk_1.default.yellow('\n Validation errors:'));
150
+ result.details.forEach((detail) => {
151
+ console.log(chalk_1.default.yellow(` • ${detail.path}: ${detail.message}`));
152
+ });
153
+ }
154
+ console.log();
155
+ }
156
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.savePlaylist = savePlaylist;
7
+ exports.loadPlaylist = loadPlaylist;
8
+ exports.formatFileSize = formatFileSize;
9
+ const fs_1 = require("fs");
10
+ const path_1 = __importDefault(require("path"));
11
+ /**
12
+ * Save playlist to a JSON file
13
+ * @param {Object} playlist - The playlist object
14
+ * @param {string} filename - Output filename
15
+ * @returns {Promise<string>} Path to the saved file
16
+ */
17
+ async function savePlaylist(playlist, filename) {
18
+ const outputPath = path_1.default.resolve(process.cwd(), filename);
19
+ // Ensure the playlist is properly formatted
20
+ const formattedPlaylist = JSON.stringify(playlist, null, 2);
21
+ await fs_1.promises.writeFile(outputPath, formattedPlaylist, 'utf-8');
22
+ return outputPath;
23
+ }
24
+ /**
25
+ * Load playlist from a JSON file
26
+ * @param {string} filename - Input filename
27
+ * @returns {Promise<Object>} The playlist object
28
+ */
29
+ async function loadPlaylist(filename) {
30
+ const filePath = path_1.default.resolve(process.cwd(), filename);
31
+ const content = await fs_1.promises.readFile(filePath, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ /**
35
+ * Format file size in human-readable format
36
+ * @param {number} bytes - Size in bytes
37
+ * @returns {string} Formatted size
38
+ */
39
+ function formatFileSize(bytes) {
40
+ const units = ['B', 'KB', 'MB', 'GB'];
41
+ let size = bytes;
42
+ let unitIndex = 0;
43
+ while (size >= 1024 && unitIndex < units.length - 1) {
44
+ size /= 1024;
45
+ unitIndex++;
46
+ }
47
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
48
+ }