or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

index.mdmusic-objects.mdnetwork-auth.mdscrobbling.mdsearch-discovery.mduser-social.md

scrobbling.mddocs/

0

# Scrobbling and Data Tracking

1

2

Music scrobbling, now playing updates, listening data management, and play count tracking. This covers PyLast's core functionality for submitting listening data to Last.fm and managing music play tracking with comprehensive timestamp and metadata support.

3

4

## Capabilities

5

6

### Basic Scrobbling

7

8

Submit listening data to Last.fm with timestamps and optional metadata for accurate music tracking.

9

10

```python { .api }

11

# Scrobbling methods available on LastFMNetwork and LibreFMNetwork

12

13

def scrobble(self, artist: str, title: str, timestamp: int, album=None, track_number=None, mbid=None, duration=None) -> None:

14

"""

15

Scrobble a single track.

16

17

Args:

18

artist (str): Artist name

19

title (str): Track title

20

timestamp (int): Unix timestamp when track was played

21

album (str, optional): Album name

22

track_number (int, optional): Track number on album

23

mbid (str, optional): MusicBrainz track ID

24

duration (int, optional): Track duration in seconds

25

26

Raises:

27

WSError: If scrobbling fails due to API error

28

NetworkError: If network connection fails

29

"""

30

31

def update_now_playing(self, artist: str, title: str, album=None, track_number=None, mbid=None, duration=None) -> None:

32

"""

33

Update now playing status without scrobbling.

34

35

Args:

36

artist (str): Artist name

37

title (str): Track title

38

album (str, optional): Album name

39

track_number (int, optional): Track number on album

40

mbid (str, optional): MusicBrainz track ID

41

duration (int, optional): Track duration in seconds

42

"""

43

```

44

45

### Batch Scrobbling

46

47

Submit multiple tracks at once for efficient bulk data submission.

48

49

```python { .api }

50

def scrobble_many(self, tracks: list[dict]) -> None:

51

"""

52

Scrobble multiple tracks at once.

53

54

Args:

55

tracks (list[dict]): List of track dictionaries, each containing:

56

- artist (str): Artist name

57

- title (str): Track title

58

- timestamp (int): Unix timestamp when played

59

- album (str, optional): Album name

60

- track_number (int, optional): Track number

61

- mbid (str, optional): MusicBrainz track ID

62

- duration (int, optional): Duration in seconds

63

64

Example:

65

tracks = [

66

{

67

'artist': 'The Beatles',

68

'title': 'Come Together',

69

'timestamp': 1234567890,

70

'album': 'Abbey Road',

71

'track_number': 1

72

},

73

{

74

'artist': 'Pink Floyd',

75

'title': 'Time',

76

'timestamp': 1234567950,

77

'album': 'The Dark Side of the Moon',

78

'duration': 421

79

}

80

]

81

network.scrobble_many(tracks)

82

"""

83

```

84

85

### Scrobble Constants

86

87

Constants for scrobble source and mode identification to provide context about listening behavior.

88

89

```python { .api }

90

# Scrobble source constants

91

SCROBBLE_SOURCE_USER = "P" # User chose the music

92

SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R" # Non-personalized broadcast (e.g., radio)

93

SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E" # Personalized broadcast (e.g., personalized radio)

94

SCROBBLE_SOURCE_LASTFM = "L" # Last.fm recommendation

95

SCROBBLE_SOURCE_UNKNOWN = "U" # Unknown source

96

97

# Scrobble mode constants

98

SCROBBLE_MODE_PLAYED = "" # Track was played normally

99

SCROBBLE_MODE_LOVED = "L" # Track was loved

100

SCROBBLE_MODE_BANNED = "B" # Track was banned/skipped

101

SCROBBLE_MODE_SKIPPED = "S" # Track was skipped

102

```

103

104

### Data Validation and Requirements

105

106

Guidelines and requirements for successful scrobbling operations.

107

108

```python { .api }

109

# Scrobbling requirements and validation

110

111

"""

112

Scrobbling Requirements:

113

- Must have valid API key and secret

114

- Must have valid session key (authenticated user)

115

- Track must be at least 30 seconds long

116

- Track must be played for at least 240 seconds OR at least half the track length

117

- Timestamp must be accurate (not future time)

118

- Artist and title are required fields

119

120

Validation Notes:

121

- Album, track number, MusicBrainz ID, and duration are optional but recommended

122

- MusicBrainz IDs provide better matching accuracy

123

- Duration should be in seconds for scrobbling, milliseconds for track objects

124

- Timestamps should be Unix timestamps (seconds since epoch)

125

126

Rate Limiting:

127

- Maximum 50 scrobbles per request with scrobble_many()

128

- API rate limit of 5 requests per second (handled automatically if rate limiting enabled)

129

- Failed scrobbles can be retried with exponential backoff

130

"""

131

```

132

133

### Utility Functions

134

135

Helper functions for scrobbling data preparation and validation.

136

137

```python { .api }

138

def md5(text: str) -> str:

139

"""

140

Create MD5 hash of text (used for password hashing).

141

142

Args:

143

text (str): Text to hash

144

145

Returns:

146

str: MD5 hash in hexadecimal format

147

148

Example:

149

password_hash = pylast.md5("my_password")

150

"""

151

152

# Time utility for timestamp generation

153

import time

154

155

def get_current_timestamp() -> int:

156

"""

157

Get current Unix timestamp for scrobbling.

158

159

Returns:

160

int: Current Unix timestamp

161

"""

162

return int(time.time())

163

```

164

165

## Data Types for Scrobbling

166

167

```python { .api }

168

from collections import namedtuple

169

170

PlayedTrack = namedtuple('PlayedTrack', ['track', 'album', 'playback_date', 'timestamp'])

171

"""

172

Represents a played track with timestamp information.

173

174

Fields:

175

track (Track): The played track object

176

album (Album): Album containing the track (may be None)

177

playback_date (str): Human-readable playback date

178

timestamp (int): Unix timestamp of when track was played

179

"""

180

```

181

182

## Usage Examples

183

184

### Basic Scrobbling

185

186

```python

187

import pylast

188

import time

189

190

# Setup authenticated network

191

network = pylast.LastFMNetwork(

192

api_key=API_KEY,

193

api_secret=API_SECRET,

194

username=username,

195

password_hash=pylast.md5(password)

196

)

197

198

# Scrobble a track that just finished playing

199

timestamp = int(time.time()) - 300 # 5 minutes ago

200

201

network.scrobble(

202

artist="Pink Floyd",

203

title="Wish You Were Here",

204

timestamp=timestamp,

205

album="Wish You Were Here",

206

track_number=1,

207

duration=334 # 5 minutes 34 seconds

208

)

209

210

print("Track scrobbled successfully!")

211

212

# Update now playing for current track

213

network.update_now_playing(

214

artist="Led Zeppelin",

215

title="Stairway to Heaven",

216

album="Led Zeppelin IV",

217

duration=482

218

)

219

220

print("Now playing updated!")

221

```

222

223

### Batch Scrobbling

224

225

```python

226

# Scrobble a listening session

227

listening_session = [

228

{

229

'artist': 'The Beatles',

230

'title': 'Come Together',

231

'timestamp': int(time.time()) - 1200, # 20 minutes ago

232

'album': 'Abbey Road',

233

'track_number': 1,

234

'duration': 259

235

},

236

{

237

'artist': 'The Beatles',

238

'title': 'Something',

239

'timestamp': int(time.time()) - 941, # ~15 minutes ago

240

'album': 'Abbey Road',

241

'track_number': 2,

242

'duration': 182

243

},

244

{

245

'artist': 'The Beatles',

246

'title': 'Maxwell\'s Silver Hammer',

247

'timestamp': int(time.time()) - 759, # ~12 minutes ago

248

'album': 'Abbey Road',

249

'track_number': 3,

250

'duration': 207

251

},

252

{

253

'artist': 'The Beatles',

254

'title': 'Oh! Darling',

255

'timestamp': int(time.time()) - 552, # ~9 minutes ago

256

'album': 'Abbey Road',

257

'track_number': 4,

258

'duration': 206

259

}

260

]

261

262

try:

263

network.scrobble_many(listening_session)

264

print(f"Successfully scrobbled {len(listening_session)} tracks!")

265

except pylast.WSError as e:

266

print(f"Scrobbling failed: {e}")

267

print(f"Error code: {e.get_id()}")

268

```

269

270

### Advanced Scrobbling with MusicBrainz IDs

271

272

```python

273

# Scrobble with MusicBrainz IDs for better accuracy

274

network.scrobble(

275

artist="Radiohead",

276

title="Paranoid Android",

277

timestamp=int(time.time()) - 383, # ~6 minutes ago

278

album="OK Computer",

279

track_number=2,

280

mbid="8cc5d2a6-1e31-4ca0-8da4-b23a8c4c0bf3", # MusicBrainz track ID

281

duration=383

282

)

283

284

# Batch scrobble with MusicBrainz data

285

mb_tracks = [

286

{

287

'artist': 'Pink Floyd',

288

'title': 'Time',

289

'timestamp': int(time.time()) - 800,

290

'album': 'The Dark Side of the Moon',

291

'track_number': 4,

292

'mbid': '0c9aa3ab-b8ab-4ae2-b777-7b139fd94f34',

293

'duration': 421

294

},

295

{

296

'artist': 'Pink Floyd',

297

'title': 'The Great Gig in the Sky',

298

'timestamp': int(time.time()) - 379,

299

'album': 'The Dark Side of the Moon',

300

'track_number': 5,

301

'mbid': '1cc3ecb0-2fdc-4fc4-a7fb-d48b4949b36a',

302

'duration': 284

303

}

304

]

305

306

network.scrobble_many(mb_tracks)

307

```

308

309

### Error Handling and Validation

310

311

```python

312

def safe_scrobble(network, artist, title, timestamp, **kwargs):

313

"""Safely scrobble with error handling and validation"""

314

315

# Basic validation

316

if not artist or not title:

317

print("Error: Artist and title are required")

318

return False

319

320

if timestamp > time.time():

321

print("Error: Cannot scrobble future timestamps")

322

return False

323

324

# Check if timestamp is too old (Last.fm has limits)

325

two_weeks_ago = time.time() - (14 * 24 * 60 * 60)

326

if timestamp < two_weeks_ago:

327

print("Warning: Timestamp is older than 2 weeks, may be rejected")

328

329

try:

330

network.scrobble(

331

artist=artist,

332

title=title,

333

timestamp=timestamp,

334

**kwargs

335

)

336

print(f"Successfully scrobbled: {artist} - {title}")

337

return True

338

339

except pylast.WSError as e:

340

error_id = e.get_id()

341

if error_id == pylast.STATUS_AUTH_FAILED:

342

print("Authentication failed - check session key")

343

elif error_id == pylast.STATUS_INVALID_PARAMS:

344

print("Invalid parameters - check artist/title")

345

elif error_id == pylast.STATUS_RATE_LIMIT_EXCEEDED:

346

print("Rate limit exceeded - wait before retrying")

347

else:

348

print(f"API error {error_id}: {e}")

349

return False

350

351

except pylast.NetworkError as e:

352

print(f"Network error: {e}")

353

return False

354

355

# Use safe scrobbling

356

safe_scrobble(

357

network,

358

artist="Queen",

359

title="Bohemian Rhapsody",

360

timestamp=int(time.time()) - 355,

361

album="A Night at the Opera",

362

duration=355

363

)

364

```

365

366

### Scrobbling from File-Based Listening Data

367

368

```python

369

import csv

370

import datetime

371

372

def scrobble_from_csv(network, csv_file):

373

"""Scrobble tracks from CSV export (e.g., from music player)"""

374

375

tracks_to_scrobble = []

376

377

with open(csv_file, 'r', encoding='utf-8') as f:

378

reader = csv.DictReader(f)

379

380

for row in reader:

381

# Parse timestamp (assuming ISO format)

382

played_at = datetime.datetime.fromisoformat(row['played_at'])

383

timestamp = int(played_at.timestamp())

384

385

track_data = {

386

'artist': row['artist'],

387

'title': row['title'],

388

'timestamp': timestamp

389

}

390

391

# Add optional fields if present

392

if row.get('album'):

393

track_data['album'] = row['album']

394

if row.get('duration'):

395

track_data['duration'] = int(row['duration'])

396

if row.get('track_number'):

397

track_data['track_number'] = int(row['track_number'])

398

399

tracks_to_scrobble.append(track_data)

400

401

# Scrobble in batches of 50 (Last.fm limit)

402

batch_size = 50

403

for i in range(0, len(tracks_to_scrobble), batch_size):

404

batch = tracks_to_scrobble[i:i + batch_size]

405

406

try:

407

network.scrobble_many(batch)

408

print(f"Scrobbled batch {i//batch_size + 1}: {len(batch)} tracks")

409

410

# Be nice to the API

411

time.sleep(1)

412

413

except Exception as e:

414

print(f"Failed to scrobble batch {i//batch_size + 1}: {e}")

415

416

print(f"Finished scrobbling {len(tracks_to_scrobble)} tracks")

417

418

# Example CSV format:

419

# artist,title,album,played_at,duration,track_number

420

# "The Beatles","Come Together","Abbey Road","2023-01-01T10:00:00","00:04:19",1

421

# scrobble_from_csv(network, "listening_history.csv")

422

```

423

424

### Real-time Scrobbling Integration

425

426

```python

427

class ScrobbleManager:

428

"""Manage real-time scrobbling for music applications"""

429

430

def __init__(self, network):

431

self.network = network

432

self.current_track = None

433

self.track_start_time = None

434

self.min_scrobble_time = 30 # Minimum seconds to scrobble

435

self.scrobble_threshold = 0.5 # Scrobble at 50% completion

436

437

def start_track(self, artist, title, album=None, duration=None):

438

"""Start playing a new track"""

439

self.current_track = {

440

'artist': artist,

441

'title': title,

442

'album': album,

443

'duration': duration

444

}

445

self.track_start_time = time.time()

446

447

# Update now playing

448

try:

449

self.network.update_now_playing(

450

artist=artist,

451

title=title,

452

album=album,

453

duration=duration

454

)

455

print(f"Now playing: {artist} - {title}")

456

except Exception as e:

457

print(f"Failed to update now playing: {e}")

458

459

def stop_track(self):

460

"""Stop current track and scrobble if criteria met"""

461

if not self.current_track or not self.track_start_time:

462

return

463

464

played_time = time.time() - self.track_start_time

465

duration = self.current_track.get('duration', 0)

466

467

# Check scrobbling criteria

468

should_scrobble = False

469

470

if played_time >= self.min_scrobble_time:

471

if duration:

472

# Scrobble if played at least 50% or 4 minutes, whichever is less

473

threshold_time = min(duration * self.scrobble_threshold, 240)

474

should_scrobble = played_time >= threshold_time

475

else:

476

# No duration info, scrobble if played more than 30 seconds

477

should_scrobble = played_time >= self.min_scrobble_time

478

479

if should_scrobble:

480

try:

481

self.network.scrobble(

482

artist=self.current_track['artist'],

483

title=self.current_track['title'],

484

timestamp=int(self.track_start_time),

485

album=self.current_track.get('album'),

486

duration=duration

487

)

488

print(f"Scrobbled: {self.current_track['artist']} - {self.current_track['title']}")

489

except Exception as e:

490

print(f"Failed to scrobble: {e}")

491

else:

492

print(f"Track not scrobbled: played {played_time:.1f}s of {duration or 'unknown'}s")

493

494

# Clear current track

495

self.current_track = None

496

self.track_start_time = None

497

498

# Usage example

499

scrobbler = ScrobbleManager(network)

500

501

# Simulate music playback

502

scrobbler.start_track("Led Zeppelin", "Stairway to Heaven", "Led Zeppelin IV", 482)

503

time.sleep(5) # Simulate 5 seconds of playback

504

scrobbler.stop_track() # Won't scrobble (too short)

505

506

scrobbler.start_track("Queen", "Bohemian Rhapsody", "A Night at the Opera", 355)

507

time.sleep(60) # Simulate 1 minute of playback

508

scrobbler.stop_track() # Will scrobble (>30 seconds and >50% of 355s)

509

```

510

511

### Scrobbling Statistics and Analysis

512

513

```python

514

def analyze_scrobbling_stats(user):

515

"""Analyze user's scrobbling patterns"""

516

517

# Get recent scrobbles

518

recent_tracks = user.get_recent_tracks(limit=100)

519

520

if not recent_tracks:

521

print("No recent tracks found")

522

return

523

524

# Analyze listening patterns

525

hourly_stats = {}

526

daily_stats = {}

527

artist_counts = {}

528

529

for played_track in recent_tracks:

530

# Parse timestamp

531

timestamp = played_track.timestamp

532

dt = datetime.datetime.fromtimestamp(timestamp)

533

534

# Hour analysis

535

hour = dt.hour

536

hourly_stats[hour] = hourly_stats.get(hour, 0) + 1

537

538

# Day analysis

539

day = dt.strftime('%A')

540

daily_stats[day] = daily_stats.get(day, 0) + 1

541

542

# Artist analysis

543

artist = played_track.track.get_artist().get_name()

544

artist_counts[artist] = artist_counts.get(artist, 0) + 1

545

546

# Print analysis

547

print(f"Scrobbling Analysis (last {len(recent_tracks)} tracks):")

548

549

print("\nMost active hours:")

550

sorted_hours = sorted(hourly_stats.items(), key=lambda x: x[1], reverse=True)

551

for hour, count in sorted_hours[:5]:

552

print(f" {hour:02d}:00 - {count} scrobbles")

553

554

print("\nMost active days:")

555

sorted_days = sorted(daily_stats.items(), key=lambda x: x[1], reverse=True)

556

for day, count in sorted_days:

557

print(f" {day} - {count} scrobbles")

558

559

print("\nTop artists:")

560

sorted_artists = sorted(artist_counts.items(), key=lambda x: x[1], reverse=True)

561

for artist, count in sorted_artists[:10]:

562

print(f" {artist} - {count} scrobbles")

563

564

# Analyze scrobbling patterns

565

user = network.get_authenticated_user()

566

analyze_scrobbling_stats(user)

567

```