Import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: make client agent string conditional on debug mode

Make the client agent string simpler in production and more detailed
in debug mode:
- Normal mode: 'malachite/v0.6.0'
- Debug mode: 'malachite/v0.6.0 (platform; Node/version)'

Changes:
- Add optional `debug` parameter to `buildClientAgent()`
- Remove static `CLIENT_AGENT` from config object
- Add `debug` parameter to conversion functions:
- `convertToPlayRecord()` (csv.ts)
- `convertSpotifyToPlayRecord()` (spotify.ts)
- `parseCombinedExports()` (merge.ts)
- Wire verbose flag through CLI to control debug mode
- Remove `CLIENT_AGENT` from Config interface

The platform and Node.js version are now only included when running
with the -v/--verbose flag, making it easier to identify records
created during debugging while keeping production records cleaner.

+23 -19
+6 -5
src/config.ts
··· 20 20 export const RECORD_TYPE = 'fm.teal.alpha.feed.play'; 21 21 22 22 // Build client agent string 23 - export function buildClientAgent() { 23 + export function buildClientAgent(debug = false) { 24 + if (!debug) { 25 + return 'malachite/v0.6.0'; 26 + } 27 + 24 28 const PLATFORM_LABELS: Record<string, string> = { 25 29 darwin: 'macOS', 26 30 linux: 'Linux', ··· 33 37 return `malachite/v0.6.0 (${platform}; Node/${process.version})`; 34 38 } 35 39 36 - const CLIENT_AGENT = buildClientAgent(); 37 - 38 40 // Default batch configuration - conservative for PDS safety 39 41 // Will dynamically adjust based on success/failure 40 42 export const DEFAULT_BATCH_SIZE = 100; // Conservative default ··· 49 51 // Slingshot resolver URL 50 52 export const SLINGSHOT_RESOLVER = 'https://slingshot.microcosm.blue'; 51 53 52 - const config: Config = { 54 + const config = { 53 55 RECORD_TYPE, 54 56 MIN_RECORDS_FOR_SCALING: 20, 55 57 BASE_BATCH_SIZE: 200, // Match DEFAULT_BATCH_SIZE for consistency 56 58 SCALING_FACTOR: 1.5, 57 - CLIENT_AGENT, 58 59 DEFAULT_BATCH_SIZE, 59 60 DEFAULT_BATCH_DELAY, 60 61 MIN_BATCH_DELAY,
+5 -3
src/lib/cli.ts
··· 291 291 let records: PlayRecord[]; 292 292 let rawRecordCount: number; 293 293 294 + const isDebug = args.verbose ?? false; 295 + 294 296 if (mode === 'combined') { 295 297 log.info('Merging Last.fm and Spotify exports...'); 296 - records = parseCombinedExports(args.input!, args['spotify-input']!, cfg); 298 + records = parseCombinedExports(args.input!, args['spotify-input']!, cfg, isDebug); 297 299 rawRecordCount = records.length; 298 300 } else if (mode === 'spotify') { 299 301 log.info('Importing from Spotify export...'); 300 302 const spotifyRecords = parseSpotifyJson(args.input!); 301 303 rawRecordCount = spotifyRecords.length; 302 - records = spotifyRecords.map(record => convertSpotifyToPlayRecord(record, cfg)); 304 + records = spotifyRecords.map(record => convertSpotifyToPlayRecord(record, cfg, isDebug)); 303 305 } else { 304 306 log.info('Importing from Last.fm CSV export...'); 305 307 const csvRecords = parseLastFmCsv(args.input!); 306 308 rawRecordCount = csvRecords.length; 307 - records = csvRecords.map(record => convertToPlayRecord(record, cfg)); 309 + records = csvRecords.map(record => convertToPlayRecord(record, cfg, isDebug)); 308 310 } 309 311 310 312 log.success(`Loaded ${rawRecordCount.toLocaleString()} records`);
+4 -3
src/lib/csv.ts
··· 2 2 import { parse } from 'csv-parse/sync'; 3 3 import type { LastFmCsvRecord, PlayRecord, Config } from '../types.js'; 4 4 import { formatDate } from '../utils/helpers.js'; 5 + import { buildClientAgent } from '../config.js'; 5 6 6 7 /** 7 8 * Parse Last.fm CSV export ··· 23 24 /** 24 25 * Convert Last.fm CSV record to ATProto play record 25 26 */ 26 - export function convertToPlayRecord(csvRecord: LastFmCsvRecord, config: Config): PlayRecord { 27 - const { RECORD_TYPE, CLIENT_AGENT } = config; 27 + export function convertToPlayRecord(csvRecord: LastFmCsvRecord, config: Config, debug = false): PlayRecord { 28 + const { RECORD_TYPE } = config; 28 29 29 30 // Parse the timestamp 30 31 const timestamp = parseInt(csvRecord.uts); ··· 48 49 trackName: csvRecord.track, 49 50 artists, 50 51 playedTime, 51 - submissionClientAgent: CLIENT_AGENT, 52 + submissionClientAgent: buildClientAgent(debug), 52 53 musicServiceBaseDomain: 'last.fm', 53 54 originUrl: '', 54 55 };
+4 -3
src/lib/merge.ts
··· 215 215 export function parseCombinedExports( 216 216 lastfmPath: string, 217 217 spotifyPath: string, 218 - config: Config 218 + config: Config, 219 + debug = false 219 220 ): PlayRecord[] { 220 221 log.section('Combined Import Mode'); 221 222 log.blank(); ··· 223 224 // Parse Last.fm 224 225 log.info('Parsing Last.fm export...'); 225 226 const lastfmCsvRecords = parseLastFmCsv(lastfmPath); 226 - const lastfmRecords = lastfmCsvRecords.map(r => convertToPlayRecord(r, config)); 227 + const lastfmRecords = lastfmCsvRecords.map(r => convertToPlayRecord(r, config, debug)); 227 228 228 229 // Parse Spotify 229 230 log.info('Parsing Spotify export...'); 230 231 const spotifyJsonRecords = parseSpotifyJson(spotifyPath); 231 - const spotifyRecords = spotifyJsonRecords.map(r => convertSpotifyToPlayRecord(r, config)); 232 + const spotifyRecords = spotifyJsonRecords.map(r => convertSpotifyToPlayRecord(r, config, debug)); 232 233 233 234 // Merge and deduplicate 234 235 const { merged, stats } = mergeRecords(lastfmRecords, spotifyRecords);
+4 -3
src/lib/spotify.ts
··· 2 2 import * as path from 'path'; 3 3 import type { PlayRecord, Config } from '../types.js'; 4 4 import { formatDate } from '../utils/helpers.js'; 5 + import { buildClientAgent } from '../config.js'; 5 6 6 7 /** 7 8 * Spotify streaming history record ··· 70 71 /** 71 72 * Convert Spotify record to ATProto play record 72 73 */ 73 - export function convertSpotifyToPlayRecord(spotifyRecord: SpotifyRecord, config: Config): PlayRecord { 74 - const { RECORD_TYPE, CLIENT_AGENT } = config; 74 + export function convertSpotifyToPlayRecord(spotifyRecord: SpotifyRecord, config: Config, debug = false): PlayRecord { 75 + const { RECORD_TYPE } = config; 75 76 76 77 // Spotify timestamp is already in ISO 8601 format 77 78 const playedTime = spotifyRecord.ts; ··· 90 91 trackName: spotifyRecord.master_metadata_track_name || 'Unknown Track', 91 92 artists, 92 93 playedTime, 93 - submissionClientAgent: CLIENT_AGENT, 94 + submissionClientAgent: buildClientAgent(debug), 94 95 musicServiceBaseDomain: 'spotify.com', 95 96 originUrl: '', 96 97 };
-2
src/types.ts
··· 88 88 SCALING_FACTOR: number; 89 89 DEFAULT_BATCH_DELAY: number; 90 90 91 - CLIENT_AGENT: string; 92 - 93 91 DEFAULT_BATCH_SIZE: number; // from rate limiter 94 92 MIN_BATCH_DELAY: number; // from rate limiter 95 93 RECORDS_PER_DAY_LIMIT: number;