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
```