Source file lastfm_generic.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
module Pcre = Re.Pcre
type client = { client : string; version : string }
type login = { user : string; password : string }
(** Decode Base64-encoded data *)
let decode64 s =
let padding = ref 0 in
let to_int c =
match c with
| 'A' .. 'Z' -> int_of_char c - int_of_char 'A'
| 'a' .. 'z' -> int_of_char c - int_of_char 'a' + 26
| '0' .. '9' -> int_of_char c - int_of_char '0' + 52
| '+' -> 62
| '/' -> 63
| '=' ->
incr padding;
0
| _ -> failwith "decode64: invalid encoding"
in
let result = ref [] in
let add x = result := char_of_int x :: !result in
for i = 0 to (String.length s / 4) - 1 do
let c n = to_int s.[(i * 4) + n] in
let i = c 3 + (c 2 lsl 6) + (c 1 lsl 12) + (c 0 lsl 18) in
add ((i land 0xff0000) lsr 16);
add ((i land 0x00ff00) lsr 8);
add (i land 0x0000ff)
done;
let result =
match !padding with
| 0 -> !result
| 1 -> List.tl !result
| 2 -> List.tl (List.tl !result)
| _ -> failwith "decode64: invalid encoding"
in
let len = List.length result in
let s = Bytes.make len ' ' in
ignore
(List.fold_left
(fun i c ->
Bytes.set s i c;
i - 1)
(len - 1) result);
Bytes.to_string s
let parse_url s =
let f h p r = (h, Some p, "/" ^ r) in
try Scanf.sscanf s "http://%[^:]:%i/%[^\r^\n]" f
with _ ->
let f h r = (h, None, "/" ^ r) in
Scanf.sscanf s "http://%[^/]/%[^\r^\n]" f
let of_hex1 c =
match c with
| '0' .. '9' -> Char.code c - Char.code '0'
| 'A' .. 'F' -> Char.code c - Char.code 'A' + 10
| 'a' .. 'f' -> Char.code c - Char.code 'a' + 10
| _ -> failwith "Error while decoding url.."
let url_decode ?(plus = true) s =
Pcre.substitute
~rex:(Pcre.regexp "\\+|%..")
~subst:(fun s ->
if s = "+" then if plus then " " else "+"
else begin
if String.length s < 3 then failwith "Error while decoding url..";
let k1 = of_hex1 s.[1] in
let k2 = of_hex1 s.[2] in
String.make 1 (Char.chr ((k1 lsl 4) lor k2))
end)
s
(** Converts k to a 2-digit hexadecimal string. *)
let to_hex2 =
let hex_digits =
[|
'0';
'1';
'2';
'3';
'4';
'5';
'6';
'7';
'8';
'9';
'A';
'B';
'C';
'D';
'E';
'F';
|]
in
fun k ->
let s = Bytes.create 2 in
Bytes.set s 0 hex_digits.((k lsr 4) land 15);
Bytes.set s 1 hex_digits.(k land 15);
Bytes.to_string s
let url_encode ?(plus = true) s =
Pcre.substitute
~rex:(Pcre.regexp "[^A-Za-z0-9_.!*-]")
~subst:(fun x ->
if plus && x = " " then "+"
else (
let k = Char.code x.[0] in
"%" ^ to_hex2 k))
s
module type Http_t = sig
type request = Get | Post of string
exception Http of string
val default_timeout : float ref
val request :
?timeout:float ->
?headers:(string * string) list ->
?port:int ->
host:string ->
url:string ->
request:request ->
unit ->
string
end
module type Audioscrobbler_t = sig
type source = User | Broadcast | Recommendation | Lastfm | Unknown
type rating = Love | Ban | Skip
type action = NowPlaying | Submit
type song = {
artist : string;
track : string;
time : float option;
source : source option;
rating : rating option;
length : float option;
album : string option;
trackauth : string option;
tracknumber : int option;
musicbrainzid : string option;
}
type error =
| Http of string
| Banned
| Badauth
| Badtime
| Failed of string
| UnknownError of string
| Success
| Internal of string
| BadData of string
exception Error of error
val string_of_error : error -> string
val base_port : int ref
val base_host : string ref
val get_song :
?time:float ->
?source:source ->
?rating:rating ->
?length:float ->
?album:string ->
?tracknumber:int ->
?musicbrainzid:string ->
?trackauth:string ->
artist:string ->
track:string ->
unit ->
song
val check_song : song -> action -> unit
val do_np :
?timeout:float -> ?host:string * int -> client -> login -> song -> unit
val do_submit :
?timeout:float ->
?host:string * int ->
client ->
login ->
song list ->
(error * song) list
val handshake :
?timeout:float -> ?host:string * int -> client -> login -> string
val np : ?timeout:float -> string -> song -> unit
val submit : ?timeout:float -> string -> song list -> (error * song) list
end
module Audioscrobbler_generic (Http : Http_t) = struct
type source = User | Broadcast | Recommendation | Lastfm | Unknown
type rating = Love | Ban | Skip
type action = NowPlaying | Submit
type song = {
artist : string;
track : string;
time : float option;
source : source option;
rating : rating option;
length : float option;
album : string option;
trackauth : string option;
tracknumber : int option;
musicbrainzid : string option;
}
type error =
| Http of string
| Banned
| Badauth
| Badtime
| Failed of string
| UnknownError of string
| Success
| Internal of string
| BadData of string
exception Error of error
let string_of_error e =
match e with
| Http s -> Printf.sprintf "http connection failed: %s" s
| Banned -> "banned client"
| Badauth -> "wrong login/password"
| Badtime -> "wrong timestamp, check your clock"
| Failed s -> Printf.sprintf "failure: %s" s
| UnknownError s -> Printf.sprintf "unknown error: %s" s
| Internal s -> Printf.sprintf "erreur interne: %s" s
| BadData s -> Printf.sprintf "Wrong song data: %s" s
| Success -> "success!"
let parse_url s =
try parse_url s
with e ->
raise
(Error
(Failed
(Printf.sprintf "Error while parsing url: %s"
(Printexc.to_string e))))
let error_of_response s =
try
let parse s =
let regexp = Pcre.regexp "FAILED\\s([^\\r\\n]*)" in
let sub = Pcre.exec ~rex:regexp s in
Failed (Pcre.get_substring sub 1)
in
let test (p, e) =
try
ignore (Pcre.exec ~rex:(Pcre.regexp p) s);
raise (Error e)
with Not_found -> ()
in
let values =
[
("OK", Success);
("BANNED", Banned);
("BADAUTH", Badauth);
("BADTIME", Badtime);
]
in
try
List.iter test values;
parse s
with Error e -> e
with Not_found -> UnknownError "unrecognized response code"
let request ?post ?timeout ? ?port ~host url =
try
let request =
match post with Some "" | None -> Http.Get | Some v -> Http.Post v
in
Http.request ?timeout ?headers ?port ~host ~url ~request ()
with Http.Http s -> raise (Error (Http s))
let version = "1.2.1"
let base_port = ref 80
let base_host = ref "post.audioscrobbler.com"
let handshake_req =
Printf.sprintf "/?hs=true&p=%s&c=%s&v=%s&u=%s&t=%s&a=%s" version
let sessions = Hashtbl.create 1
let urls = Hashtbl.create 1
let raise e = raise (Error e)
let arg_value_string x = match x with Some e -> e | None -> ""
let arg_value_int x = match x with Some e -> string_of_int e | None -> ""
let arg_value_float x =
match x with Some e -> Printf.sprintf "%.0f" e | None -> ""
let clear sessionid =
let keys =
Hashtbl.fold
(fun a b r -> if b = sessionid then a :: r else r)
sessions []
in
ignore (List.map (fun x -> Hashtbl.remove sessions x) keys);
Hashtbl.remove urls sessionid
let handshake ?timeout ?host client login =
let client, version, user, pass =
(client.client, client.version, login.user, login.password)
in
let host, port =
match host with Some (x, y) -> (x, y) | None -> (!base_host, !base_port)
in
try Hashtbl.find sessions (user, pass, (host, port))
with Not_found -> (
let timestamp = Printf.sprintf "%.0f" (Unix.time ()) in
let pass_digest = Digest.string pass in
let token = Digest.string (Digest.to_hex pass_digest ^ timestamp) in
let req =
handshake_req client version user timestamp (Digest.to_hex token)
in
let ans = request ?timeout ~host ~port req in
let state, id, v =
try
let lines = Pcre.split ~rex:(Pcre.regexp "[\r\n]+") ans in
match lines with
| [state; id; a; b] -> (state, id, (a, b))
| _ -> raise (error_of_response ans)
with Not_found -> raise (error_of_response ans)
in
match error_of_response state with
| Success ->
Hashtbl.replace sessions (user, pass, (host, port)) id;
Hashtbl.replace urls id v;
id
| e -> raise e)
let check_song s a =
match s.source with
| None when a = Submit ->
raise (BadData "source field is required for submit action")
| _ -> (
();
match s.length with
| None when a = Submit && s.source = Some User ->
raise (BadData "length required when source is User")
| _ -> (
();
match s.time with
| None when s.source = Some User ->
raise (BadData "time required when source is User")
| _ -> (
();
match s.trackauth with
| None when s.source = Some Lastfm ->
raise
(BadData
"trackauth required when source is Lastfm")
| _ -> (
();
match s.rating with
| (Some Ban | Some Skip)
when s.source <> Some Lastfm ->
raise
(BadData
"bad rating value (ban and skip are for \
lastfm sources only)")
| _ -> ()))))
let audioscrobbler_post ?timeout id base_url values =
let host, port, req = parse_url base_url in
let port = match port with Some x -> x | None -> !base_port in
let args =
List.map (fun (a, b) -> Printf.sprintf "%s=%s" a (url_encode b)) values
in
let post = String.concat "&" args in
let = [("Content-type", "application/x-www-form-urlencoded")] in
let ans = request ?timeout ~post ~headers ~host ~port req in
match error_of_response ans with
| Success -> ()
| e ->
clear id;
raise e
let np ?timeout id song =
let url, _ = try Hashtbl.find urls id with Not_found -> raise Badauth in
check_song song NowPlaying;
let values =
[
("s", id);
("a", song.artist);
("t", song.track);
("album", arg_value_string song.album);
("l", arg_value_float song.length);
("n", arg_value_int song.tracknumber);
("m", arg_value_string song.musicbrainzid);
]
in
audioscrobbler_post ?timeout id url values
let submit ?timeout id songs =
let _, url =
try Hashtbl.find urls id
with Not_found ->
raise (Internal (Printf.sprintf "No session data for session ID %s" id))
in
let count = ref 0 in
let failed = ref [] in
let args = ref [("s", id)] in
let get_arg s = Printf.sprintf "%s[%d]" s !count in
let add_arg song =
try
check_song song Submit;
let ar, tr, t, s, r, l, al, n, m, x =
( song.artist,
song.track,
song.time,
song.source,
song.rating,
song.length,
song.album,
song.tracknumber,
song.musicbrainzid,
song.trackauth )
in
let l = match l with None -> "" | Some s -> Printf.sprintf "%.0f" s in
let t = match t with Some t -> Printf.sprintf "%.0f" t | None -> "" in
let x = match x with Some x -> x | None -> "" in
let s =
match s with
| Some User -> "P"
| Some Broadcast -> "R"
| Some Recommendation -> "E"
| Some Lastfm -> "L"
| Some Unknown -> "U"
| None -> raise (Internal "source field is required for submit")
in
let r =
match r with
| Some Love -> "L"
| Some Ban -> "B"
| Some Skip -> "S"
| None -> ""
in
args :=
[
(get_arg "a", ar);
(get_arg "t", tr);
(get_arg "i", t);
(get_arg "o", s ^ x);
(get_arg "r", r);
(get_arg "l", l);
(get_arg "b", arg_value_string al);
(get_arg "n", arg_value_int n);
(get_arg "m", arg_value_string m);
]
@ !args
with Error e -> failed := (e, song) :: !failed
in
List.iter add_arg songs;
audioscrobbler_post ?timeout id url !args;
!failed
let get_song ?time ?source ?rating ?length ?album ?tracknumber ?musicbrainzid
?trackauth ~artist ~track () =
{
artist;
track;
time;
source;
rating;
length;
album;
tracknumber;
musicbrainzid;
trackauth;
}
let do_np ?timeout ?host client login song =
let id = handshake ?host client login in
try np ?timeout id song
with Error Badauth ->
clear id;
let id = handshake ?host client login in
np id song
let do_submit ?timeout ?host client login songs =
let id = handshake ?host client login in
try submit ?timeout id songs
with Error Badauth ->
clear id;
let id = handshake ?host client login in
submit id songs
end
module type Radio_t = sig
type track = (string * string) list * string
type error =
| Http of string
| Auth of string
| Adjust of string * string
| Playlist
| Empty
exception Error of error
val string_of_error : error -> string
val base_host : string ref
val get : ?timeout:float -> string -> track list
val parse : string -> login * string * string option
val init : ?timeout:float -> login -> string
val adjust : ?timeout:float -> string -> string -> (string * string) list
val playlist : ?timeout:float -> string -> string option -> string
val tracks : ?timeout:float -> string -> string option -> track list
val clear : string -> unit
end
module Radio_generic (Http : Http_t) = struct
type track = (string * string) list * string
type session = {
login : login;
station : ((string * string) list * string) option;
base_url : string;
playlist_url : string option;
base_path : string;
}
type error =
| Http of string
| Auth of string
| Adjust of string * string
| Playlist
| Empty
exception Error of error
exception Internal of string
let string_of_error e =
match e with
| Http s -> Printf.sprintf "http connection failed: %s" s
| Auth s -> Printf.sprintf "could not open session:\n%s" s
| Adjust (s, s') ->
Printf.sprintf
"could not adjust station to %s:\n%s\nIs the URI valid ?" s s'
| Playlist -> "error while parsing the playlist"
| Empty -> "no files available"
let request ?post ?timeout ? ?port ~host url =
try
let request =
match post with Some "" | None -> Http.Get | Some v -> Http.Post v
in
Http.request ?timeout ?headers ?port ~host ~url ~request ()
with Http.Http s -> raise (Error (Http s))
let parse_url s =
try parse_url s
with e ->
raise
(Error
(Http
(Printf.sprintf "Error while parsing url: %s"
(Printexc.to_string e))))
let url_encode s =
try url_encode s with
| Failure s ->
raise (Error (Http (Printf.sprintf "Error while parsing url: %s" s)))
| e ->
raise
(Error
(Http
(Printf.sprintf "Error while parsing url: %s"
(Printexc.to_string e))))
let _raise = raise
let raise e = raise (Error e)
let base_host = ref "ext.last.fm"
let sessions = Hashtbl.create 10
let registered_handshake =
Printf.sprintf "/radio/handshake.php?username=%s&passwordmd5=%s"
let station_set base id url =
Printf.sprintf "%s/adjust.php?session=%s&url=%s" base id url
let playlist_req id options =
let d =
try Hashtbl.find sessions id
with Not_found ->
raise (Auth (Printf.sprintf "No session registered for id %s" id))
in
let options = match options with Some s -> s | None -> "" in
let url =
match d.playlist_url with
| None -> "http://ws.audioscrobbler.com/radio/xspf.php"
| Some s -> s
in
Printf.sprintf "%s?sk=%s&%s" url id options
let playlist ?timeout id options =
let url = playlist_req id options in
let host, port, req = parse_url url in
let port = match port with Some x -> x | None -> 80 in
let data = request ?timeout ~port ~host req in
let data = decode64 data in
url_decode data
let parse_args s =
let rex = Pcre.regexp "[&\n]" in
let values = Pcre.split ~rex s in
let split s l =
try
let sub = Pcre.exec ~rex:(Pcre.regexp "([^=]*)=(.*)") s in
(Pcre.get_substring sub 1, Pcre.get_substring sub 2) :: l
with Not_found -> l
in
List.fold_right split values []
let parse_handshake s =
let params = parse_args s in
try
let f x = List.assoc x params in
let g x = try Some (f x) with Not_found -> None in
(f "session", g "playlist_url", f "base_url", f "base_path")
with Not_found -> raise (Auth s)
let adjust_pat = "response=OK"
let check_adjust s = Pcre.pmatch ~rex:(Pcre.regexp adjust_pat) s
let opt_split_rex = Pcre.regexp "^([^?]+)\\?(.+)$"
let opt_parse s =
try
let sub = Pcre.exec ~rex:opt_split_rex s in
(Pcre.get_substring sub 1, Some (Pcre.get_substring sub 2))
with Not_found -> (s, None)
let auth_split_rex = Pcre.regexp "^lastfm://([^:]+):([^@]+)@(.+)$"
let parse uri =
try
let sub = Pcre.exec ~rex:auth_split_rex uri in
let data = Pcre.get_substring sub 3 in
let station, options = opt_parse data in
let user, password =
(Pcre.get_substring sub 1, Pcre.get_substring sub 2)
in
({ user; password }, Printf.sprintf "lastfm://%s" station, options)
with Not_found -> raise (Auth "Could not find login/password.")
let clear id = Hashtbl.remove sessions id
let init ?timeout login =
try
Hashtbl.iter
(fun x d -> if d.login = login then _raise (Internal x))
sessions;
_raise Not_found
with
| Not_found ->
let user, password = (login.user, login.password) in
let password = Digest.to_hex (Digest.string password) in
let ret =
request ?timeout ~host:!base_host
(registered_handshake (url_encode user) password)
in
let id, playlist_url, base_url, base_path = parse_handshake ret in
Hashtbl.replace sessions id
{ playlist_url; base_url; base_path; login; station = None };
id
| Internal x -> x
let adjust ?timeout id req =
let d =
try Hashtbl.find sessions id
with Not_found ->
raise (Auth (Printf.sprintf "No session registered for id %s" id))
in
let base_url, base_path = (d.base_url, d.base_path) in
match d.station with
| Some (a, s) when s = req -> a
| _ ->
let http_req = station_set base_path id (url_encode req) in
let ret = request ?timeout ~host:base_url http_req in
if check_adjust ret then (
let args = parse_args ret in
Hashtbl.replace sessions id
{
login = d.login;
station = Some (args, req);
playlist_url = d.playlist_url;
base_url;
base_path;
};
args)
else begin
clear id;
raise (Adjust (req, ret))
end
let tracks ?timeout id options =
try
let playlist = playlist ?timeout id options in
Xmlplaylist.tracks playlist
with
| Xmlplaylist.Error _ ->
clear id;
raise Playlist
| Error e ->
clear id;
raise e
let get ?timeout uri =
let login, station, options = parse uri in
let id = init ?timeout login in
try
ignore (adjust ?timeout id station);
tracks ?timeout id options
with Error _ ->
clear id;
let id = init login in
ignore (adjust id station);
tracks id options
end