Completed
Push — jacebrowning-auth-token ( a66cea )
by Hugo
05:12
created

User.__init__()   A

Complexity

Conditions 1

Size

Total Lines 9

Duplication

Lines 5
Ratio 55.56 %

Code Coverage

Tests 1
CRAP Score 1.6296

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 5
loc 9
rs 9.6666
ccs 1
cts 7
cp 0.1429
crap 1.6296
1
# -*- coding: utf-8 -*-
2
#
3
# pylast -
4
#     A Python interface to Last.fm and Libre.fm
5
#
6
# Copyright 2008-2010 Amr Hassan
7
# Copyright 2013-2017 hugovk
8
#
9
# Licensed under the Apache License, Version 2.0 (the "License");
10
# you may not use this file except in compliance with the License.
11
# You may obtain a copy of the License at
12
#
13
#     http://www.apache.org/licenses/LICENSE-2.0
14
#
15
# Unless required by applicable law or agreed to in writing, software
16
# distributed under the License is distributed on an "AS IS" BASIS,
17
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
# See the License for the specific language governing permissions and
19
# limitations under the License.
20
#
21
# https://github.com/pylast/pylast
22
23 1
import hashlib
24 1
from xml.dom import minidom, Node
25 1
import xml.dom
26 1
import time
27 1
import shelve
28 1
import tempfile
29 1
import sys
30 1
import collections
31 1
import warnings
32 1
import re
33 1
import six
34
35 1
__version__ = '1.7.0'
36 1
__author__ = 'Amr Hassan, hugovk'
37 1
__copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2017 hugovk"
38 1
__license__ = "apache2"
39 1
__email__ = '[email protected]'
40
41
42 1
def _deprecation_warning(message):
43
    warnings.warn(message, DeprecationWarning)
44
45
46 1
def _can_use_ssl_securely():
47
    # Python 3.3 doesn't support create_default_context() but can be made to
48
    # work sanely.
49
    # <2.7.9 and <3.2 never did any SSL verification so don't do SSL there.
50
    # >3.4 and >2.7.9 has sane defaults so use SSL there.
51 1
    v = sys.version_info
52 1
    return v > (3, 3) or ((2, 7, 9) < v < (3, 0))
53
54 1
if _can_use_ssl_securely():
55 1
    import ssl
56
57 1
if sys.version_info[0] == 3:
58
    if _can_use_ssl_securely():
59
        from http.client import HTTPSConnection
60
    else:
61
        from http.client import HTTPConnection
62
    import html.entities as htmlentitydefs
63
    from urllib.parse import splithost as url_split_host
64
    from urllib.parse import quote_plus as url_quote_plus
65
66
    unichr = chr
67
68 1
elif sys.version_info[0] == 2:
69 1
    if _can_use_ssl_securely():
70 1
        from httplib import HTTPSConnection
71
    else:
72
        from httplib import HTTPConnection
73 1
    import htmlentitydefs
74 1
    from urllib import splithost as url_split_host
75 1
    from urllib import quote_plus as url_quote_plus
76
77 1
STATUS_INVALID_SERVICE = 2
78 1
STATUS_INVALID_METHOD = 3
79 1
STATUS_AUTH_FAILED = 4
80 1
STATUS_INVALID_FORMAT = 5
81 1
STATUS_INVALID_PARAMS = 6
82 1
STATUS_INVALID_RESOURCE = 7
83 1
STATUS_TOKEN_ERROR = 8
84 1
STATUS_INVALID_SK = 9
85 1
STATUS_INVALID_API_KEY = 10
86 1
STATUS_OFFLINE = 11
87 1
STATUS_SUBSCRIBERS_ONLY = 12
88 1
STATUS_INVALID_SIGNATURE = 13
89 1
STATUS_TOKEN_UNAUTHORIZED = 14
90 1
STATUS_TOKEN_EXPIRED = 15
91
92 1
EVENT_ATTENDING = '0'
93 1
EVENT_MAYBE_ATTENDING = '1'
94 1
EVENT_NOT_ATTENDING = '2'
95
96 1
PERIOD_OVERALL = 'overall'
97 1
PERIOD_7DAYS = '7day'
98 1
PERIOD_1MONTH = '1month'
99 1
PERIOD_3MONTHS = '3month'
100 1
PERIOD_6MONTHS = '6month'
101 1
PERIOD_12MONTHS = '12month'
102
103 1
DOMAIN_ENGLISH = 0
104 1
DOMAIN_GERMAN = 1
105 1
DOMAIN_SPANISH = 2
106 1
DOMAIN_FRENCH = 3
107 1
DOMAIN_ITALIAN = 4
108 1
DOMAIN_POLISH = 5
109 1
DOMAIN_PORTUGUESE = 6
110 1
DOMAIN_SWEDISH = 7
111 1
DOMAIN_TURKISH = 8
112 1
DOMAIN_RUSSIAN = 9
113 1
DOMAIN_JAPANESE = 10
114 1
DOMAIN_CHINESE = 11
115
116 1
COVER_SMALL = 0
117 1
COVER_MEDIUM = 1
118 1
COVER_LARGE = 2
119 1
COVER_EXTRA_LARGE = 3
120 1
COVER_MEGA = 4
121
122 1
IMAGES_ORDER_POPULARITY = "popularity"
123 1
IMAGES_ORDER_DATE = "dateadded"
124
125
126 1
USER_MALE = 'Male'
127 1
USER_FEMALE = 'Female'
128
129 1
SCROBBLE_SOURCE_USER = "P"
130 1
SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R"
131 1
SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E"
132 1
SCROBBLE_SOURCE_LASTFM = "L"
133 1
SCROBBLE_SOURCE_UNKNOWN = "U"
134
135 1
SCROBBLE_MODE_PLAYED = ""
136 1
SCROBBLE_MODE_LOVED = "L"
137 1
SCROBBLE_MODE_BANNED = "B"
138 1
SCROBBLE_MODE_SKIPPED = "S"
139
140
# From http://boodebr.org/main/python/all-about-python-and-unicode#UNI_XML
141 1
RE_XML_ILLEGAL = (u'([\u0000-\u0008\u000b-\u000c\u000e-\u001f\ufffe-\uffff])' +
142
                  u'|' +
143
                  u'([%s-%s][^%s-%s])|([^%s-%s][%s-%s])|([%s-%s]$)|(^[%s-%s])'
144
                  %
145
                  (unichr(0xd800), unichr(0xdbff), unichr(0xdc00),
146
                   unichr(0xdfff), unichr(0xd800), unichr(0xdbff),
147
                   unichr(0xdc00), unichr(0xdfff), unichr(0xd800),
148
                   unichr(0xdbff), unichr(0xdc00), unichr(0xdfff)))
149
150 1
XML_ILLEGAL = re.compile(RE_XML_ILLEGAL)
151
152
# Python <=3.3 doesn't support create_default_context()
153
# <2.7.9 and <3.2 never did any SSL verification
154
# FIXME This can be removed after 2017-09 when 3.3 is no longer supported and
155
# pypy3 uses 3.4 or later, see
156
# https://en.wikipedia.org/wiki/CPython#Version_history
157 1
if sys.version_info[0] == 3 and sys.version_info[1] == 3:
158
    import certifi
159
    SSL_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
160
    SSL_CONTEXT.verify_mode = ssl.CERT_REQUIRED
161
    SSL_CONTEXT.options |= ssl.OP_NO_COMPRESSION
162
    # Intermediate from https://wiki.mozilla.org/Security/Server_Side_TLS
163
    # Create the cipher string
164
    cipher_string = """
165
    ECDHE-ECDSA-CHACHA20-POLY1305
166
    ECDHE-RSA-CHACHA20-POLY1305
167
    ECDHE-ECDSA-AES128-GCM-SHA256
168
    ECDHE-RSA-AES128-GCM-SHA256
169
    ECDHE-ECDSA-AES256-GCM-SHA384
170
    ECDHE-RSA-AES256-GCM-SHA384
171
    DHE-RSA-AES128-GCM-SHA256
172
    DHE-RSA-AES256-GCM-SHA384
173
    ECDHE-ECDSA-AES128-SHA256
174
    ECDHE-RSA-AES128-SHA256
175
    ECDHE-ECDSA-AES128-SHA
176
    ECDHE-RSA-AES256-SHA384
177
    ECDHE-RSA-AES128-SHA
178
    ECDHE-ECDSA-AES256-SHA384
179
    ECDHE-ECDSA-AES256-SHA
180
    ECDHE-RSA-AES256-SHA
181
    DHE-RSA-AES128-SHA256
182
    DHE-RSA-AES128-SHA
183
    DHE-RSA-AES256-SHA256
184
    DHE-RSA-AES256-SHA
185
    ECDHE-ECDSA-DES-CBC3-SHA
186
    ECDHE-RSA-DES-CBC3-SHA
187
    EDH-RSA-DES-CBC3-SHA
188
    AES128-GCM-SHA256
189
    AES256-GCM-SHA384
190
    AES128-SHA256
191
    AES256-SHA256
192
    AES128-SHA
193
    AES256-SHA
194
    DES-CBC3-SHA
195
    !DSS
196
    """
197
    cipher_string = ' '.join(cipher_string.split())
198
    SSL_CONTEXT.set_ciphers(cipher_string)
199
    SSL_CONTEXT.load_verify_locations(certifi.where())
200
201
# Python >3.4 and >2.7.9 has sane defaults
202 1
elif sys.version_info > (3, 4) or ((2, 7, 9) < sys.version_info < (3, 0)):
203 1
    SSL_CONTEXT = ssl.create_default_context()
204
205
206 1
class _Network(object):
207
    """
208
    A music social network website such as Last.fm or
209
    one with a Last.fm-compatible API.
210
    """
211
212 1
    def __init__(
213
            self, name, homepage, ws_server, api_key, api_secret, session_key,
214
            submission_server, username, password_hash, token, domain_names,
215
            urls):
216
        """
217
            name: the name of the network
218
            homepage: the homepage URL
219
            ws_server: the URL of the webservices server
220
            api_key: a provided API_KEY
221
            api_secret: a provided API_SECRET
222
            session_key: a generated session_key or None
223
            submission_server: the URL of the server to which tracks are
224
                submitted (scrobbled)
225
            username: a username of a valid user
226
            password_hash: the output of pylast.md5(password) where password is
227
                the user's password
228
            token: an authentication token to retrieve a session
229
            domain_names: a dict mapping each DOMAIN_* value to a string domain
230
                name
231
            urls: a dict mapping types to URLs
232
233
            if username and password_hash were provided and not session_key,
234
            session_key will be generated automatically when needed.
235
236
            Either a valid session_key or a combination of username and
237
            password_hash must be present for scrobbling.
238
239
            You should use a preconfigured network object through a
240
            get_*_network(...) method instead of creating an object
241
            of this class, unless you know what you're doing.
242
        """
243
244
        self.name = name
245
        self.homepage = homepage
246
        self.ws_server = ws_server
247
        self.api_key = api_key
248
        self.api_secret = api_secret
249
        self.session_key = session_key
250
        self.submission_server = submission_server
251
        self.username = username
252
        self.password_hash = password_hash
253
        self.domain_names = domain_names
254
        self.urls = urls
255
256
        self.cache_backend = None
257
        self.proxy_enabled = False
258
        self.proxy = None
259
        self.last_call_time = 0
260
        self.limit_rate = False
261
262
        # Load session_key from authentication token if provided
263
        if token and not self.session_key:
264
            sk_gen = SessionKeyGenerator(self)
265
            self.session_key = sk_gen.get_web_auth_session_key(
266
                url=None, token=token)
267
268
        # Generate a session_key if necessary
269
        if ((self.api_key and self.api_secret) and not self.session_key and
270
           (self.username and self.password_hash)):
271
            sk_gen = SessionKeyGenerator(self)
272
            self.session_key = sk_gen.get_session_key(
273
                self.username, self.password_hash)
274
275 1
    def __str__(self):
276
        return "%s Network" % self.name
277
278 1
    def get_artist(self, artist_name):
279
        """
280
            Return an Artist object
281
        """
282
283
        return Artist(artist_name, self)
284
285 1
    def get_track(self, artist, title):
286
        """
287
            Return a Track object
288
        """
289
290
        return Track(artist, title, self)
291
292 1
    def get_album(self, artist, title):
293
        """
294
            Return an Album object
295
        """
296
297
        return Album(artist, title, self)
298
299 1
    def get_authenticated_user(self):
300
        """
301
            Returns the authenticated user
302
        """
303
304
        return AuthenticatedUser(self)
305
306 1
    def get_country(self, country_name):
307
        """
308
            Returns a country object
309
        """
310
311
        return Country(country_name, self)
312
313 1
    def get_metro(self, metro_name, country_name):
314
        """
315
            Returns a metro object
316
        """
317
318
        return Metro(metro_name, country_name, self)
319
320 1
    def get_group(self, name):
321
        """
322
            Returns a Group object
323
        """
324
325
        return Group(name, self)
326
327 1
    def get_user(self, username):
328
        """
329
            Returns a user object
330
        """
331
332
        return User(username, self)
333
334 1
    def get_tag(self, name):
335
        """
336
            Returns a tag object
337
        """
338
339
        return Tag(name, self)
340
341 1
    def get_scrobbler(self, client_id, client_version):
342
        """
343
            Returns a Scrobbler object used for submitting tracks to the server
344
345
            Quote from http://www.last.fm/api/submissions:
346
            ========
347
            Client identifiers are used to provide a centrally managed database
348
            of the client versions, allowing clients to be banned if they are
349
            found to be behaving undesirably. The client ID is associated with
350
            a version number on the server, however these are only incremented
351
            if a client is banned and do not have to reflect the version of the
352
            actual client application.
353
354
            During development, clients which have not been allocated an
355
            identifier should use the identifier tst, with a version number of
356
            1.0. Do not distribute code or client implementations which use
357
            this test identifier. Do not use the identifiers used by other
358
            clients.
359 View Code Duplication
            =========
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
360
361
            To obtain a new client identifier please contact:
362
                * Last.fm: [email protected]
363
                * # TODO: list others
364
365
            ...and provide us with the name of your client and its homepage
366
            address.
367
        """
368
369
        _deprecation_warning(
370
            "Use _Network.scrobble(...), _Network.scrobble_many(...),"
371
            " and Network.update_now_playing(...) instead")
372
373
        return Scrobbler(self, client_id, client_version)
374
375 1
    def _get_language_domain(self, domain_language):
376
        """
377
            Returns the mapped domain name of the network to a DOMAIN_* value
378
        """
379
380
        if domain_language in self.domain_names:
381
            return self.domain_names[domain_language]
382
383 1
    def _get_url(self, domain, url_type):
384
        return "http://%s/%s" % (
385
            self._get_language_domain(domain), self.urls[url_type])
386
387 1
    def _get_ws_auth(self):
388
        """
389
            Returns an (API_KEY, API_SECRET, SESSION_KEY) tuple.
390
        """
391
        return (self.api_key, self.api_secret, self.session_key)
392
393 1
    def _delay_call(self):
394
        """
395
            Makes sure that web service calls are at least 0.2 seconds apart.
396
        """
397
398
        # Delay time in seconds from section 4.4 of http://www.last.fm/api/tos
399
        DELAY_TIME = 0.2
400
        now = time.time()
401
402
        time_since_last = now - self.last_call_time
403
404
        if time_since_last < DELAY_TIME:
405
            time.sleep(DELAY_TIME - time_since_last)
406
407
        self.last_call_time = now
408
409 1
    def create_new_playlist(self, title, description):
410
        """
411
            Creates a playlist for the authenticated user and returns it
412
                title: The title of the new playlist.
413
                description: The description of the new playlist.
414
        """
415
416
        params = {}
417
        params['title'] = title
418
        params['description'] = description
419
420
        doc = _Request(self, 'playlist.create', params).execute(False)
421
422
        e_id = doc.getElementsByTagName("id")[0].firstChild.data
423
        user = doc.getElementsByTagName('playlists')[0].getAttribute('user')
424
425
        return Playlist(user, e_id, self)
426
427 1
    def get_top_artists(self, limit=None, cacheable=True):
428
        """Returns the most played artists as a sequence of TopItem objects."""
429
430
        params = {}
431
        if limit:
432
            params["limit"] = limit
433
434
        doc = _Request(self, "chart.getTopArtists", params).execute(cacheable)
435
436
        return _extract_top_artists(doc, self)
437
438 1
    def get_top_tracks(self, limit=None, cacheable=True):
439
        """Returns the most played tracks as a sequence of TopItem objects."""
440
441
        params = {}
442
        if limit:
443
            params["limit"] = limit
444
445
        doc = _Request(self, "chart.getTopTracks", params).execute(cacheable)
446
447
        seq = []
448
        for node in doc.getElementsByTagName("track"):
449
            title = _extract(node, "name")
450
            artist = _extract(node, "name", 1)
451
            track = Track(artist, title, self)
452
            weight = _number(_extract(node, "playcount"))
453
            seq.append(TopItem(track, weight))
454
455
        return seq
456
457 1
    def get_top_tags(self, limit=None, cacheable=True):
458
        """Returns the most used tags as a sequence of TopItem objects."""
459
460
        # Last.fm has no "limit" parameter for tag.getTopTags
461
        # so we need to get all (250) and then limit locally
462
        doc = _Request(self, "tag.getTopTags").execute(cacheable)
463
464
        seq = []
465
        for node in doc.getElementsByTagName("tag"):
466
            if limit and len(seq) >= limit:
467
                break
468
            tag = Tag(_extract(node, "name"), self)
469
            weight = _number(_extract(node, "count"))
470
            seq.append(TopItem(tag, weight))
471
472
        return seq
473
474 1
    def get_geo_events(
475
            self, longitude=None, latitude=None, location=None, distance=None,
476
            tag=None, festivalsonly=None, limit=None, cacheable=True):
477
        """
478
        Returns all events in a specific location by country or city name.
479
        Parameters:
480
        longitude (Optional) : Specifies a longitude value to retrieve events
481
            for (service returns nearby events by default)
482
        latitude (Optional) : Specifies a latitude value to retrieve events for
483
            (service returns nearby events by default)
484
        location (Optional) : Specifies a location to retrieve events for
485
            (service returns nearby events by default)
486
        distance (Optional) : Find events within a specified radius
487
            (in kilometres)
488
        tag (Optional) : Specifies a tag to filter by.
489
        festivalsonly[0|1] (Optional) : Whether only festivals should be
490
            returned, or all events.
491
        limit (Optional) : The number of results to fetch per page.
492
            Defaults to 10.
493
        """
494
495
        params = {}
496 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
497
        if longitude:
498
            params["long"] = longitude
499
        if latitude:
500
            params["lat"] = latitude
501
        if location:
502
            params["location"] = location
503
        if limit:
504
            params["limit"] = limit
505
        if distance:
506
            params["distance"] = distance
507
        if tag:
508
            params["tag"] = tag
509
        if festivalsonly:
510
            params["festivalsonly"] = 1
511
        elif not festivalsonly:
512
            params["festivalsonly"] = 0
513
514
        doc = _Request(self, "geo.getEvents", params).execute(cacheable)
515
516
        return _extract_events_from_doc(doc, self)
517
518 1
    def get_metro_weekly_chart_dates(self, cacheable=True):
519
        """
520
        Returns a list of From and To tuples for the available metro charts.
521
        """
522
523
        doc = _Request(self, "geo.getMetroWeeklyChartlist").execute(cacheable)
524
525
        seq = []
526
        for node in doc.getElementsByTagName("chart"):
527
            seq.append((node.getAttribute("from"), node.getAttribute("to")))
528
529
        return seq
530
531 1
    def get_metros(self, country=None, cacheable=True):
532
        """
533
        Get a list of valid countries and metros for use in the other
534
        webservices.
535
        Parameters:
536
        country (Optional) : Optionally restrict the results to those Metros
537
            from a particular country, as defined by the ISO 3166-1 country
538
            names standard.
539
        """
540
        params = {}
541
542
        if country:
543
            params["country"] = country
544
545
        doc = _Request(self, "geo.getMetros", params).execute(cacheable)
546
547
        metros = doc.getElementsByTagName("metro")
548
        seq = []
549
550
        for metro in metros:
551
            name = _extract(metro, "name")
552
            country = _extract(metro, "country")
553
554
            seq.append(Metro(name, country, self))
555
556
        return seq
557
558 1
    def get_geo_top_artists(self, country, limit=None, cacheable=True):
559
        """Get the most popular artists on Last.fm by country.
560
        Parameters:
561
        country (Required) : A country name, as defined by the ISO 3166-1
562
            country names standard.
563
        limit (Optional) : The number of results to fetch per page.
564
            Defaults to 50.
565
        """
566
        params = {"country": country}
567
568
        if limit:
569
            params["limit"] = limit
570
571
        doc = _Request(self, "geo.getTopArtists", params).execute(cacheable)
572
573
        return _extract_top_artists(doc, self)
574
575 1
    def get_geo_top_tracks(
576
            self, country, location=None, limit=None, cacheable=True):
577
        """Get the most popular tracks on Last.fm last week by country.
578
        Parameters:
579
        country (Required) : A country name, as defined by the ISO 3166-1
580
            country names standard
581
        location (Optional) : A metro name, to fetch the charts for
582
            (must be within the country specified)
583
        limit (Optional) : The number of results to fetch per page.
584
            Defaults to 50.
585
        """
586
        params = {"country": country}
587
588
        if location:
589
            params["location"] = location
590
        if limit:
591
            params["limit"] = limit
592
593
        doc = _Request(self, "geo.getTopTracks", params).execute(cacheable)
594
595
        tracks = doc.getElementsByTagName("track")
596
        seq = []
597
598
        for track in tracks:
599
            title = _extract(track, "name")
600
            artist = _extract(track, "name", 1)
601
            listeners = _extract(track, "listeners")
602
603
            seq.append(TopItem(Track(artist, title, self), listeners))
604
605
        return seq
606
607 1
    def enable_proxy(self, host, port):
608
        """Enable a default web proxy"""
609
610
        self.proxy = [host, _number(port)]
611
        self.proxy_enabled = True
612
613 1
    def disable_proxy(self):
614
        """Disable using the web proxy"""
615
616
        self.proxy_enabled = False
617
618 1
    def is_proxy_enabled(self):
619
        """Returns True if a web proxy is enabled."""
620
621
        return self.proxy_enabled
622
623 1
    def _get_proxy(self):
624
        """Returns proxy details."""
625
626
        return self.proxy
627
628 1
    def enable_rate_limit(self):
629
        """Enables rate limiting for this network"""
630
        self.limit_rate = True
631
632 1
    def disable_rate_limit(self):
633
        """Disables rate limiting for this network"""
634
        self.limit_rate = False
635
636 1
    def is_rate_limited(self):
637
        """Return True if web service calls are rate limited"""
638
        return self.limit_rate
639
640 1
    def enable_caching(self, file_path=None):
641
        """Enables caching request-wide for all cacheable calls.
642
643
        * file_path: A file path for the backend storage file. If
644
        None set, a temp file would probably be created, according the backend.
645
        """
646
647
        if not file_path:
648
            file_path = tempfile.mktemp(prefix="pylast_tmp_")
649
650
        self.cache_backend = _ShelfCacheBackend(file_path)
651
652 1
    def disable_caching(self):
653
        """Disables all caching features."""
654
655
        self.cache_backend = None
656
657 1
    def is_caching_enabled(self):
658
        """Returns True if caching is enabled."""
659
660
        return not (self.cache_backend is None)
661
662 1
    def _get_cache_backend(self):
663
664
        return self.cache_backend
665
666 1
    def search_for_album(self, album_name):
667
        """Searches for an album by its name. Returns a AlbumSearch object.
668
        Use get_next_page() to retrieve sequences of results."""
669
670
        return AlbumSearch(album_name, self)
671
672 1
    def search_for_artist(self, artist_name):
673
        """Searches of an artist by its name. Returns a ArtistSearch object.
674
        Use get_next_page() to retrieve sequences of results."""
675
676
        return ArtistSearch(artist_name, self)
677
678 1
    def search_for_tag(self, tag_name):
679
        """Searches of a tag by its name. Returns a TagSearch object.
680
        Use get_next_page() to retrieve sequences of results."""
681
682
        return TagSearch(tag_name, self)
683
684 1
    def search_for_track(self, artist_name, track_name):
685
        """Searches of a track by its name and its artist. Set artist to an
686
        empty string if not available.
687
        Returns a TrackSearch object.
688
        Use get_next_page() to retrieve sequences of results."""
689
690
        return TrackSearch(artist_name, track_name, self)
691
692 1
    def search_for_venue(self, venue_name, country_name):
693
        """Searches of a venue by its name and its country. Set country_name to
694
        an empty string if not available.
695
        Returns a VenueSearch object.
696
        Use get_next_page() to retrieve sequences of results."""
697
698
        return VenueSearch(venue_name, country_name, self)
699
700 1
    def get_track_by_mbid(self, mbid):
701
        """Looks up a track by its MusicBrainz ID"""
702
703
        params = {"mbid": mbid}
704
705
        doc = _Request(self, "track.getInfo", params).execute(True)
706
707
        return Track(_extract(doc, "name", 1), _extract(doc, "name"), self)
708
709 1
    def get_artist_by_mbid(self, mbid):
710
        """Loooks up an artist by its MusicBrainz ID"""
711
712
        params = {"mbid": mbid}
713
714
        doc = _Request(self, "artist.getInfo", params).execute(True)
715
716
        return Artist(_extract(doc, "name"), self)
717
718 1
    def get_album_by_mbid(self, mbid):
719
        """Looks up an album by its MusicBrainz ID"""
720
721
        params = {"mbid": mbid}
722
723
        doc = _Request(self, "album.getInfo", params).execute(True)
724
725
        return Album(_extract(doc, "artist"), _extract(doc, "name"), self)
726
727 1
    def update_now_playing(
728
            self, artist, title, album=None, album_artist=None,
729
            duration=None, track_number=None, mbid=None, context=None):
730
        """
731
        Used to notify Last.fm that a user has started listening to a track.
732
733
            Parameters:
734
                artist (Required) : The artist name
735
                title (Required) : The track title
736
                album (Optional) : The album name.
737
                album_artist (Optional) : The album artist - if this differs
738
                    from the track artist.
739
                duration (Optional) : The length of the track in seconds.
740
                track_number (Optional) : The track number of the track on the
741
                    album.
742
                mbid (Optional) : The MusicBrainz Track ID.
743
                context (Optional) : Sub-client version
744
                    (not public, only enabled for certain API keys)
745
        """
746
747
        params = {"track": title, "artist": artist}
748
749
        if album:
750
            params["album"] = album
751
        if album_artist:
752
            params["albumArtist"] = album_artist
753
        if context:
754
            params["context"] = context
755
        if track_number:
756
            params["trackNumber"] = track_number
757
        if mbid:
758
            params["mbid"] = mbid
759
        if duration:
760
            params["duration"] = duration
761
762
        _Request(self, "track.updateNowPlaying", params).execute()
763
764 1
    def scrobble(
765
            self, artist, title, timestamp, album=None, album_artist=None,
766
            track_number=None, duration=None, stream_id=None, context=None,
767
            mbid=None):
768
769
        """Used to add a track-play to a user's profile.
770
771
        Parameters:
772
            artist (Required) : The artist name.
773
            title (Required) : The track name.
774
            timestamp (Required) : The time the track started playing, in UNIX
775
                timestamp format (integer number of seconds since 00:00:00,
776
                January 1st 1970 UTC). This must be in the UTC time zone.
777
            album (Optional) : The album name.
778
            album_artist (Optional) : The album artist - if this differs from
779
                the track artist.
780
            context (Optional) : Sub-client version (not public, only enabled
781
                for certain API keys)
782
            stream_id (Optional) : The stream id for this track received from
783
                the radio.getPlaylist service.
784
            track_number (Optional) : The track number of the track on the
785
                album.
786
            mbid (Optional) : The MusicBrainz Track ID.
787
            duration (Optional) : The length of the track in seconds.
788
        """
789
790
        return self.scrobble_many(({
791
            "artist": artist, "title": title, "timestamp": timestamp,
792
            "album": album, "album_artist": album_artist,
793
            "track_number": track_number, "duration": duration,
794
            "stream_id": stream_id, "context": context, "mbid": mbid},))
795
796 1
    def scrobble_many(self, tracks):
797
        """
798
        Used to scrobble a batch of tracks at once. The parameter tracks is a
799
        sequence of dicts per track containing the keyword arguments as if
800
        passed to the scrobble() method.
801
        """
802
803
        tracks_to_scrobble = tracks[:50]
804
        if len(tracks) > 50:
805
            remaining_tracks = tracks[50:]
806
        else:
807
            remaining_tracks = None
808
809
        params = {}
810
        for i in range(len(tracks_to_scrobble)):
811
812
            params["artist[%d]" % i] = tracks_to_scrobble[i]["artist"]
813
            params["track[%d]" % i] = tracks_to_scrobble[i]["title"]
814
815
            additional_args = (
816
                "timestamp", "album", "album_artist", "context",
817
                "stream_id", "track_number", "mbid", "duration")
818
            args_map_to = {  # so friggin lazy
819
                "album_artist": "albumArtist",
820
                "track_number": "trackNumber",
821
                "stream_id": "streamID"}
822
823
            for arg in additional_args:
824
825
                if arg in tracks_to_scrobble[i] and tracks_to_scrobble[i][arg]:
826
                    if arg in args_map_to:
827
                        maps_to = args_map_to[arg]
828
                    else:
829
                        maps_to = arg
830
831
                    params[
832
                        "%s[%d]" % (maps_to, i)] = tracks_to_scrobble[i][arg]
833
834
        _Request(self, "track.scrobble", params).execute()
835
836
        if remaining_tracks:
837
            self.scrobble_many(remaining_tracks)
838
839 1
    def get_play_links(self, link_type, things, cacheable=True):
840
        method = link_type + ".getPlaylinks"
841
        params = {}
842
843
        for i, thing in enumerate(things):
844
            if link_type == "artist":
845
                params['artist[' + str(i) + ']'] = thing
846
            elif link_type == "album":
847
                params['artist[' + str(i) + ']'] = thing.artist
848
                params['album[' + str(i) + ']'] = thing.title
849
            elif link_type == "track":
850
                params['artist[' + str(i) + ']'] = thing.artist
851
                params['track[' + str(i) + ']'] = thing.title
852
853
        doc = _Request(self, method, params).execute(cacheable)
854
855
        seq = []
856
857
        for node in doc.getElementsByTagName("externalids"):
858
            spotify = _extract(node, "spotify")
859
            seq.append(spotify)
860
861
        return seq
862
863 1
    def get_artist_play_links(self, artists, cacheable=True):
864
        return self.get_play_links("artist", artists, cacheable)
865
866 1
    def get_album_play_links(self, albums, cacheable=True):
867
        return self.get_play_links("album", albums, cacheable)
868
869 1
    def get_track_play_links(self, tracks, cacheable=True):
870
        return self.get_play_links("track", tracks, cacheable)
871
872
873 1 View Code Duplication
class LastFMNetwork(_Network):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
874
875
    """A Last.fm network object
876
877
    api_key: a provided API_KEY
878
    api_secret: a provided API_SECRET
879
    session_key: a generated session_key or None
880
    username: a username of a valid user
881
    password_hash: the output of pylast.md5(password) where password is the
882
        user's password
883
884
    if username and password_hash were provided and not session_key,
885
    session_key will be generated automatically when needed.
886
887
    Either a valid session_key or a combination of username and password_hash
888
    must be present for scrobbling.
889
890
    Most read-only webservices only require an api_key and an api_secret, see
891
    about obtaining them from:
892
    http://www.last.fm/api/account
893
    """
894
895 1
    def __init__(
896
            self, api_key="", api_secret="", session_key="", username="",
897
            password_hash="", token=""):
898
        _Network.__init__(
899
            self,
900
            name="Last.fm",
901
            homepage="http://last.fm",
902
            ws_server=("ws.audioscrobbler.com", "/2.0/"),
903
            api_key=api_key,
904
            api_secret=api_secret,
905
            session_key=session_key,
906
            submission_server="http://post.audioscrobbler.com:80/",
907
            username=username,
908
            password_hash=password_hash,
909
            token=token,
910
            domain_names={
911
                DOMAIN_ENGLISH: 'www.last.fm',
912
                DOMAIN_GERMAN: 'www.lastfm.de',
913
                DOMAIN_SPANISH: 'www.lastfm.es',
914
                DOMAIN_FRENCH: 'www.lastfm.fr',
915
                DOMAIN_ITALIAN: 'www.lastfm.it',
916
                DOMAIN_POLISH: 'www.lastfm.pl',
917
                DOMAIN_PORTUGUESE: 'www.lastfm.com.br',
918
                DOMAIN_SWEDISH: 'www.lastfm.se',
919
                DOMAIN_TURKISH: 'www.lastfm.com.tr',
920
                DOMAIN_RUSSIAN: 'www.lastfm.ru',
921
                DOMAIN_JAPANESE: 'www.lastfm.jp',
922
                DOMAIN_CHINESE: 'cn.last.fm',
923
            },
924
            urls={
925
                "album": "music/%(artist)s/%(album)s",
926
                "artist": "music/%(artist)s",
927
                "event": "event/%(id)s",
928
                "country": "place/%(country_name)s",
929
                "playlist": "user/%(user)s/library/playlists/%(appendix)s",
930
                "tag": "tag/%(name)s",
931
                "track": "music/%(artist)s/_/%(title)s",
932
                "group": "group/%(name)s",
933
                "user": "user/%(name)s",
934
            }
935
        )
936
937 1
    def __repr__(self):
938
        return "pylast.LastFMNetwork(%s)" % (", ".join(
939
            ("'%s'" % self.api_key,
940
             "'%s'" % self.api_secret,
941
             "'%s'" % self.session_key,
942
             "'%s'" % self.username,
943
             "'%s'" % self.password_hash,
944
             "'%s'" % self.token)))
945
946
947 1
def get_lastfm_network(
948
        api_key="", api_secret="", session_key="", username="",
949
        password_hash="", token=""):
950
    """
951
    Returns a preconfigured _Network object for Last.fm
952
953
    api_key: a provided API_KEY
954
    api_secret: a provided API_SECRET
955
    session_key: a generated session_key or None
956
    username: a username of a valid user
957
    password_hash: the output of pylast.md5(password) where password is the
958
        user's password
959
    token: an authentication token to retrieve a session
960
961
    if username and password_hash were provided and not session_key,
962
    session_key will be generated automatically when needed.
963
964
    Either a valid session_key, a combination of username and password_hash,
965
    or token must be present for scrobbling.
966
967
    Most read-only webservices only require an api_key and an api_secret, see
968
    about obtaining them from:
969
    http://www.last.fm/api/account
970
    """
971
972
    _deprecation_warning("Create a LastFMNetwork object instead")
973
974
    return LastFMNetwork(
975
        api_key, api_secret, session_key, username, password_hash, token)
976
977
978 1 View Code Duplication
class LibreFMNetwork(_Network):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
979
    """
980
    A preconfigured _Network object for Libre.fm
981
982
    api_key: a provided API_KEY
983
    api_secret: a provided API_SECRET
984
    session_key: a generated session_key or None
985
    username: a username of a valid user
986
    password_hash: the output of pylast.md5(password) where password is the
987
        user's password
988
989
    if username and password_hash were provided and not session_key,
990
    session_key will be generated automatically when needed.
991
    """
992
993 1
    def __init__(
994
            self, api_key="", api_secret="", session_key="", username="",
995
            password_hash=""):
996
997
        _Network.__init__(
998
            self,
999
            name="Libre.fm",
1000
            homepage="http://libre.fm",
1001
            ws_server=("libre.fm", "/2.0/"),
1002
            api_key=api_key,
1003
            api_secret=api_secret,
1004
            session_key=session_key,
1005
            submission_server="http://turtle.libre.fm:80/",
1006
            username=username,
1007
            password_hash=password_hash,
1008
            domain_names={
1009
                DOMAIN_ENGLISH: "libre.fm",
1010
                DOMAIN_GERMAN: "libre.fm",
1011
                DOMAIN_SPANISH: "libre.fm",
1012
                DOMAIN_FRENCH: "libre.fm",
1013
                DOMAIN_ITALIAN: "libre.fm",
1014
                DOMAIN_POLISH: "libre.fm",
1015
                DOMAIN_PORTUGUESE: "libre.fm",
1016
                DOMAIN_SWEDISH: "libre.fm",
1017
                DOMAIN_TURKISH: "libre.fm",
1018
                DOMAIN_RUSSIAN: "libre.fm",
1019
                DOMAIN_JAPANESE: "libre.fm",
1020
                DOMAIN_CHINESE: "libre.fm",
1021
            },
1022
            urls={
1023
                "album": "artist/%(artist)s/album/%(album)s",
1024
                "artist": "artist/%(artist)s",
1025
                "event": "event/%(id)s",
1026
                "country": "place/%(country_name)s",
1027
                "playlist": "user/%(user)s/library/playlists/%(appendix)s",
1028
                "tag": "tag/%(name)s",
1029
                "track": "music/%(artist)s/_/%(title)s",
1030
                "group": "group/%(name)s",
1031
                "user": "user/%(name)s",
1032
            }
1033
        )
1034
1035 1
    def __repr__(self):
1036
        return "pylast.LibreFMNetwork(%s)" % (", ".join(
1037
            ("'%s'" % self.api_key,
1038
             "'%s'" % self.api_secret,
1039
             "'%s'" % self.session_key,
1040
             "'%s'" % self.username,
1041
             "'%s'" % self.password_hash)))
1042
1043
1044 1
def get_librefm_network(
1045
        api_key="", api_secret="", session_key="", username="",
1046
        password_hash=""):
1047
    """
1048
    Returns a preconfigured _Network object for Libre.fm
1049
1050
    api_key: a provided API_KEY
1051
    api_secret: a provided API_SECRET
1052
    session_key: a generated session_key or None
1053
    username: a username of a valid user
1054
    password_hash: the output of pylast.md5(password) where password is the
1055
        user's password
1056
1057
    if username and password_hash were provided and not session_key,
1058
    session_key will be generated automatically when needed.
1059
    """
1060
1061
    _deprecation_warning(
1062
        "DeprecationWarning: Create a LibreFMNetwork object instead")
1063
1064
    return LibreFMNetwork(
1065
        api_key, api_secret, session_key, username, password_hash)
1066
1067
1068 1
class _ShelfCacheBackend(object):
1069
    """Used as a backend for caching cacheable requests."""
1070 1
    def __init__(self, file_path=None):
1071
        self.shelf = shelve.open(file_path)
1072
1073 1
    def __iter__(self):
1074
        return iter(self.shelf.keys())
1075
1076 1
    def get_xml(self, key):
1077
        return self.shelf[key]
1078
1079 1
    def set_xml(self, key, xml_string):
1080
        self.shelf[key] = xml_string
1081
1082
1083 1
class _Request(object):
1084
    """Representing an abstract web service operation."""
1085
1086 1
    def __init__(self, network, method_name, params={}):
1087
1088 1
        self.network = network
1089 1
        self.params = {}
1090
1091 1
        for key in params:
1092 1
            self.params[key] = _unicode(params[key])
1093
1094 1
        (self.api_key, self.api_secret, self.session_key) = \
1095
            network._get_ws_auth()
1096
1097 1
        self.params["api_key"] = self.api_key
1098 1
        self.params["method"] = method_name
1099
1100 1
        if network.is_caching_enabled():
1101 1
            self.cache = network._get_cache_backend()
1102
1103 1
        if self.session_key:
1104
            self.params["sk"] = self.session_key
1105
            self.sign_it()
1106
1107 1
    def sign_it(self):
1108
        """Sign this request."""
1109
1110
        if "api_sig" not in self.params.keys():
1111
            self.params['api_sig'] = self._get_signature()
1112
1113 1
    def _get_signature(self):
1114
        """
1115
        Returns a 32-character hexadecimal md5 hash of the signature string.
1116
        """
1117
1118
        keys = list(self.params.keys())
1119
1120
        keys.sort()
1121
1122
        string = ""
1123
1124
        for name in keys:
1125
            string += name
1126
            string += self.params[name]
1127
1128
        string += self.api_secret
1129
1130
        return md5(string)
1131
1132 1
    def _get_cache_key(self):
1133
        """
1134
        The cache key is a string of concatenated sorted names and values.
1135
        """
1136
1137 1
        keys = list(self.params.keys())
1138 1
        keys.sort()
1139
1140 1
        cache_key = str()
1141
1142 1
        for key in keys:
1143 1
            if key != "api_sig" and key != "api_key" and key != "sk":
1144 1
                cache_key += key + self.params[key]
1145
1146 1
        return hashlib.sha1(cache_key.encode("utf-8")).hexdigest()
1147
1148 1
    def _get_cached_response(self):
1149
        """Returns a file object of the cached response."""
1150
1151
        if not self._is_cached():
1152
            response = self._download_response()
1153
            self.cache.set_xml(self._get_cache_key(), response)
1154
1155
        return self.cache.get_xml(self._get_cache_key())
1156
1157 1
    def _is_cached(self):
1158
        """Returns True if the request is already in cache."""
1159
1160
        return self._get_cache_key() in self.cache
1161
1162 1
    def _download_response(self):
1163
        """Returns a response body string from the server."""
1164
1165
        if self.network.limit_rate:
1166
            self.network._delay_call()
1167
1168
        data = []
1169
        for name in self.params.keys():
1170
            data.append('='.join((
1171
                name, url_quote_plus(_string(self.params[name])))))
1172
        data = '&'.join(data)
1173
1174
        headers = {
1175
            "Content-type": "application/x-www-form-urlencoded",
1176
            'Accept-Charset': 'utf-8',
1177
            'User-Agent': "pylast" + '/' + __version__
1178
        }
1179
1180
        (HOST_NAME, HOST_SUBDIR) = self.network.ws_server
1181
1182
        if self.network.is_proxy_enabled():
1183
            if _can_use_ssl_securely():
1184
                conn = HTTPSConnection(
1185
                    context=SSL_CONTEXT,
1186
                    host=self.network._get_proxy()[0],
1187
                    port=self.network._get_proxy()[1])
1188
            else:
1189
                conn = HTTPConnection(
1190
                    host=self.network._get_proxy()[0],
1191
                    port=self.network._get_proxy()[1])
1192
1193
            try:
1194
                conn.request(
1195
                    method='POST', url="http://" + HOST_NAME + HOST_SUBDIR,
1196
                    body=data, headers=headers)
1197
            except Exception as e:
1198
                raise NetworkError(self.network, e)
1199
1200
        else:
1201
            if _can_use_ssl_securely():
1202
                conn = HTTPSConnection(
1203
                    context=SSL_CONTEXT,
1204
                    host=HOST_NAME
1205
                )
1206
            else:
1207
                conn = HTTPConnection(
1208
                    host=HOST_NAME
1209
                )
1210
1211
            try:
1212
                conn.request(
1213
                    method='POST', url=HOST_SUBDIR, body=data, headers=headers)
1214
            except Exception as e:
1215
                raise NetworkError(self.network, e)
1216
1217
        try:
1218
            response_text = _unicode(conn.getresponse().read())
1219
        except Exception as e:
1220
            raise MalformedResponseError(self.network, e)
1221
1222
        response_text = XML_ILLEGAL.sub("?", response_text)
1223
1224
        self._check_response_for_errors(response_text)
1225
        return response_text
1226
1227 1
    def execute(self, cacheable=False):
1228
        """Returns the XML DOM response of the POST Request from the server"""
1229
1230
        if self.network.is_caching_enabled() and cacheable:
1231
            response = self._get_cached_response()
1232
        else:
1233
            response = self._download_response()
1234
1235
        return minidom.parseString(_string(response).replace(
1236
            "opensearch:", ""))
1237
1238 1
    def _check_response_for_errors(self, response):
1239
        """Checks the response for errors and raises one if any exists."""
1240
1241
        try:
1242
            doc = minidom.parseString(_string(response).replace(
1243
                "opensearch:", ""))
1244
        except Exception as e:
1245
            raise MalformedResponseError(self.network, e)
1246
1247
        e = doc.getElementsByTagName('lfm')[0]
1248
1249
        if e.getAttribute('status') != "ok":
1250
            e = doc.getElementsByTagName('error')[0]
1251
            status = e.getAttribute('code')
1252
            details = e.firstChild.data.strip()
1253
            raise WSError(self.network, status, details)
1254
1255
1256 1
class SessionKeyGenerator(object):
1257
    """Methods of generating a session key:
1258
    1) Web Authentication:
1259
        a. network = get_*_network(API_KEY, API_SECRET)
1260
        b. sg = SessionKeyGenerator(network)
1261
        c. url = sg.get_web_auth_url()
1262
        d. Ask the user to open the url and authorize you, and wait for it.
1263
        e. session_key = sg.get_web_auth_session_key(url)
1264
    2) Username and Password Authentication:
1265
        a. network = get_*_network(API_KEY, API_SECRET)
1266
        b. username = raw_input("Please enter your username: ")
1267
        c. password_hash = pylast.md5(raw_input("Please enter your password: ")
1268
        d. session_key = SessionKeyGenerator(network).get_session_key(username,
1269
            password_hash)
1270
1271
    A session key's lifetime is infinite, unless the user revokes the rights
1272
    of the given API Key.
1273
1274
    If you create a Network object with just a API_KEY and API_SECRET and a
1275
    username and a password_hash, a SESSION_KEY will be automatically generated
1276
    for that network and stored in it so you don't have to do this manually,
1277
    unless you want to.
1278
    """
1279
1280 1
    def __init__(self, network):
1281
        self.network = network
1282
        self.web_auth_tokens = {}
1283
1284 1
    def _get_web_auth_token(self):
1285
        """
1286
        Retrieves a token from the network for web authentication.
1287
        The token then has to be authorized from getAuthURL before creating
1288
        session.
1289
        """
1290
1291
        request = _Request(self.network, 'auth.getToken')
1292
1293
        # default action is that a request is signed only when
1294
        # a session key is provided.
1295
        request.sign_it()
1296
1297
        doc = request.execute()
1298
1299
        e = doc.getElementsByTagName('token')[0]
1300
        return e.firstChild.data
1301
1302 1
    def get_web_auth_url(self):
1303
        """
1304
        The user must open this page, and you first, then
1305
        call get_web_auth_session_key(url) after that.
1306
        """
1307
1308
        token = self._get_web_auth_token()
1309
1310
        url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \
1311
            {"homepage": self.network.homepage,
1312
             "api": self.network.api_key, "token": token}
1313
1314
        self.web_auth_tokens[url] = token
1315
1316
        return url
1317
1318 1
    def get_web_auth_session_key(self, url, token=""):
1319
        """
1320
        Retrieves the session key of a web authorization process by its url.
1321
        """
1322
1323
        if url in self.web_auth_tokens.keys():
1324
            token = self.web_auth_tokens[url]
1325
        else:
1326
            # This will raise a WSError if token is blank or unauthorized
1327
            token = token
1328
1329
        request = _Request(self.network, 'auth.getSession', {'token': token})
1330
1331
        # default action is that a request is signed only when
1332
        # a session key is provided.
1333
        request.sign_it()
1334
1335
        doc = request.execute()
1336
1337
        return doc.getElementsByTagName('key')[0].firstChild.data
1338
1339 1
    def get_session_key(self, username, password_hash):
1340
        """
1341
        Retrieve a session key with a username and a md5 hash of the user's
1342
        password.
1343
        """
1344
1345
        params = {
1346
            "username": username, "authToken": md5(username + password_hash)}
1347
        request = _Request(self.network, "auth.getMobileSession", params)
1348
1349
        # default action is that a request is signed only when
1350
        # a session key is provided.
1351
        request.sign_it()
1352
1353
        doc = request.execute()
1354
1355
        return _extract(doc, "key")
1356
1357
1358 1
TopItem = collections.namedtuple("TopItem", ["item", "weight"])
1359 1
SimilarItem = collections.namedtuple("SimilarItem", ["item", "match"])
1360 1
LibraryItem = collections.namedtuple(
1361
    "LibraryItem", ["item", "playcount", "tagcount"])
1362 1
PlayedTrack = collections.namedtuple(
1363
    "PlayedTrack", ["track", "album", "playback_date", "timestamp"])
1364 1
LovedTrack = collections.namedtuple(
1365
    "LovedTrack", ["track", "date", "timestamp"])
1366 1
ImageSizes = collections.namedtuple(
1367
    "ImageSizes", [
1368
        "original", "large", "largesquare", "medium", "small", "extralarge"])
1369 1
Image = collections.namedtuple(
1370
    "Image", [
1371
        "title", "url", "dateadded", "format", "owner", "sizes", "votes"])
1372 1
Shout = collections.namedtuple(
1373
    "Shout", ["body", "author", "date"])
1374
1375
1376 1
def _string_output(funct):
1377 1
    def r(*args):
1378
        return _string(funct(*args))
1379
1380 1
    return r
1381
1382
1383 1
def _pad_list(given_list, desired_length, padding=None):
1384
    """
1385
        Pads a list to be of the desired_length.
1386
    """
1387
1388
    while len(given_list) < desired_length:
1389
        given_list.append(padding)
1390
1391
    return given_list
1392
1393
1394 1
class _BaseObject(object):
1395
    """An abstract webservices object."""
1396
1397 1
    network = None
1398
1399 1
    def __init__(self, network, ws_prefix):
1400 1
        self.network = network
1401 1
        self.ws_prefix = ws_prefix
1402
1403 1
    def _request(self, method_name, cacheable=False, params=None):
1404
        if not params:
1405
            params = self._get_params()
1406
1407
        return _Request(self.network, method_name, params).execute(cacheable)
1408
1409 1
    def _get_params(self):
1410
        """Returns the most common set of parameters between all objects."""
1411
1412
        return {}
1413
1414 1
    def __hash__(self):
1415
        # Convert any ints (or whatever) into strings
1416 1
        values = map(six.text_type, self._get_params().values())
1417
1418 1
        return hash(self.network) + hash(six.text_type(type(self)) + "".join(
1419
            list(self._get_params().keys()) + list(values)
1420
        ).lower())
1421
1422 1
    def _extract_cdata_from_request(self, method_name, tag_name, params):
1423
        doc = self._request(method_name, True, params)
1424
1425
        return doc.getElementsByTagName(
1426
            tag_name)[0].firstChild.wholeText.strip()
1427
1428 1
    def _get_things(
1429
            self, method, thing, thing_type, params=None, cacheable=True):
1430
        """Returns a list of the most played thing_types by this thing."""
1431
1432
        doc = self._request(
1433
            self.ws_prefix + "." + method, cacheable, params)
1434
1435
        seq = []
1436
        for node in doc.getElementsByTagName(thing):
1437
            title = _extract(node, "name")
1438
            artist = _extract(node, "name", 1)
1439
            playcount = _number(_extract(node, "playcount"))
1440
1441
            seq.append(TopItem(
1442
                thing_type(artist, title, self.network), playcount))
1443
1444
        return seq
1445
1446 1
    def get_top_fans(self, limit=None, cacheable=True):
1447
        """Returns a list of the Users who played this the most.
1448
        # Parameters:
1449
            * limit int: Max elements.
1450
        # For Artist/Track
1451
        """
1452
1453
        doc = self._request(self.ws_prefix + '.getTopFans', cacheable)
1454
1455
        seq = []
1456
1457
        elements = doc.getElementsByTagName('user')
1458
1459
        for element in elements:
1460
            if limit and len(seq) >= limit:
1461
                break
1462
1463
            name = _extract(element, 'name')
1464
            weight = _number(_extract(element, 'weight'))
1465
1466
            seq.append(TopItem(User(name, self.network), weight))
1467
1468
        return seq
1469
1470 1
    def share(self, users, message=None):
1471
        """
1472
        Shares this (sends out recommendations).
1473
        Parameters:
1474
            * users [User|str,]: A list that can contain usernames, emails,
1475
            User objects, or all of them.
1476
            * message str: A message to include in the recommendation message.
1477
        Only for Artist/Event/Track.
1478
        """
1479
1480
        # Last.fm currently accepts a max of 10 recipient at a time
1481
        while(len(users) > 10):
1482
            section = users[0:9]
1483
            users = users[9:]
1484
            self.share(section, message)
1485
1486
        nusers = []
1487
        for user in users:
1488
            if isinstance(user, User):
1489
                nusers.append(user.get_name())
1490
            else:
1491
                nusers.append(user)
1492
1493
        params = self._get_params()
1494
        recipients = ','.join(nusers)
1495
        params['recipient'] = recipients
1496
        if message:
1497
            params['message'] = message
1498
1499
        self._request(self.ws_prefix + '.share', False, params)
1500
1501 1
    def get_wiki_published_date(self):
1502
        """
1503
        Returns the summary of the wiki.
1504
        Only for Album/Track.
1505
        """
1506
        return self.get_wiki("published")
1507
1508 1
    def get_wiki_summary(self):
1509
        """
1510
        Returns the summary of the wiki.
1511
        Only for Album/Track.
1512
        """
1513
        return self.get_wiki("summary")
1514
1515 1
    def get_wiki_content(self):
1516
        """
1517
        Returns the summary of the wiki.
1518
        Only for Album/Track.
1519
        """
1520
        return self.get_wiki("content")
1521
1522 1
    def get_wiki(self, section):
1523
        """
1524
        Returns a section of the wiki.
1525
        Only for Album/Track.
1526
        section can be "content", "summary" or
1527
            "published" (for published date)
1528
        """
1529
1530
        doc = self._request(self.ws_prefix + ".getInfo", True)
1531
1532
        if len(doc.getElementsByTagName("wiki")) == 0:
1533
            return
1534
1535
        node = doc.getElementsByTagName("wiki")[0]
1536
1537
        return _extract(node, section)
1538
1539 1
    def get_shouts(self, limit=50, cacheable=False):
1540
        """
1541
            Returns a sequence of Shout objects
1542
        """
1543
1544
        shouts = []
1545
        for node in _collect_nodes(
1546
                limit,
1547
                self,
1548
                self.ws_prefix + ".getShouts",
1549
                cacheable):
1550
            shouts.append(
1551
                Shout(
1552
                    _extract(node, "body"),
1553
                    User(_extract(node, "author"), self.network),
1554
                    _extract(node, "date")
1555
                )
1556
            )
1557
        return shouts
1558
1559
1560 1
class _Chartable(object):
1561
    """Common functions for classes with charts."""
1562
1563 1
    def __init__(self, ws_prefix):
1564
        self.ws_prefix = ws_prefix  # TODO move to _BaseObject?
1565
1566 1
    def get_weekly_chart_dates(self):
1567
        """Returns a list of From and To tuples for the available charts."""
1568
1569
        doc = self._request(self.ws_prefix + ".getWeeklyChartList", True)
1570
1571
        seq = []
1572
        for node in doc.getElementsByTagName("chart"):
1573
            seq.append((node.getAttribute("from"), node.getAttribute("to")))
1574
1575
        return seq
1576
1577 1
    def get_weekly_album_charts(self, from_date=None, to_date=None):
1578
        """
1579
        Returns the weekly album charts for the week starting from the
1580
        from_date value to the to_date value.
1581
        Only for Group or User.
1582
        """
1583
        return self.get_weekly_charts("album", from_date, to_date)
1584
1585 1
    def get_weekly_artist_charts(self, from_date=None, to_date=None):
1586
        """
1587
        Returns the weekly artist charts for the week starting from the
1588
        from_date value to the to_date value.
1589
        Only for Group, Tag or User.
1590
        """
1591
        return self.get_weekly_charts("artist", from_date, to_date)
1592
1593 1
    def get_weekly_track_charts(self, from_date=None, to_date=None):
1594
        """
1595
        Returns the weekly track charts for the week starting from the
1596
        from_date value to the to_date value.
1597
        Only for Group or User.
1598
        """
1599
        return self.get_weekly_charts("track", from_date, to_date)
1600
1601 1
    def get_weekly_charts(self, chart_kind, from_date=None, to_date=None):
1602
        """
1603
        Returns the weekly charts for the week starting from the
1604
        from_date value to the to_date value.
1605
        chart_kind should be one of "album", "artist" or "track"
1606
        """
1607
        method = ".getWeekly" + chart_kind.title() + "Chart"
1608
        chart_type = eval(chart_kind.title())  # string to type
1609
1610
        params = self._get_params()
1611
        if from_date and to_date:
1612
            params["from"] = from_date
1613
            params["to"] = to_date
1614
1615
        doc = self._request(
1616
            self.ws_prefix + method, True, params)
1617
1618
        seq = []
1619
        for node in doc.getElementsByTagName(chart_kind.lower()):
1620
            item = chart_type(
1621
                _extract(node, "artist"), _extract(node, "name"), self.network)
1622
            weight = _number(_extract(node, "playcount"))
1623
            seq.append(TopItem(item, weight))
1624
1625
        return seq
1626
1627
1628 1
class _Taggable(object):
1629
    """Common functions for classes with tags."""
1630
1631 1
    def __init__(self, ws_prefix):
1632 1
        self.ws_prefix = ws_prefix  # TODO move to _BaseObject
1633
1634 1
    def add_tags(self, tags):
1635
        """Adds one or several tags.
1636
        * tags: A sequence of tag names or Tag objects.
1637
        """
1638
1639
        for tag in tags:
1640
            self.add_tag(tag)
1641
1642 1
    def add_tag(self, tag):
1643
        """Adds one tag.
1644
        * tag: a tag name or a Tag object.
1645
        """
1646
1647
        if isinstance(tag, Tag):
1648
            tag = tag.get_name()
1649
1650
        params = self._get_params()
1651
        params['tags'] = tag
1652
1653
        self._request(self.ws_prefix + '.addTags', False, params)
1654
1655 1
    def remove_tag(self, tag):
1656
        """Remove a user's tag from this object."""
1657
1658
        if isinstance(tag, Tag):
1659
            tag = tag.get_name()
1660
1661
        params = self._get_params()
1662
        params['tag'] = tag
1663
1664
        self._request(self.ws_prefix + '.removeTag', False, params)
1665
1666 1
    def get_tags(self):
1667
        """Returns a list of the tags set by the user to this object."""
1668
1669
        # Uncacheable because it can be dynamically changed by the user.
1670
        params = self._get_params()
1671
1672
        doc = self._request(self.ws_prefix + '.getTags', False, params)
1673
        tag_names = _extract_all(doc, 'name')
1674
        tags = []
1675
        for tag in tag_names:
1676
            tags.append(Tag(tag, self.network))
1677
1678
        return tags
1679
1680 1
    def remove_tags(self, tags):
1681
        """Removes one or several tags from this object.
1682
        * tags: a sequence of tag names or Tag objects.
1683
        """
1684
1685
        for tag in tags:
1686
            self.remove_tag(tag)
1687
1688 1
    def clear_tags(self):
1689
        """Clears all the user-set tags. """
1690
1691
        self.remove_tags(*(self.get_tags()))
1692
1693 1
    def set_tags(self, tags):
1694
        """Sets this object's tags to only those tags.
1695
        * tags: a sequence of tag names or Tag objects.
1696
        """
1697
1698
        c_old_tags = []
1699
        old_tags = []
1700
        c_new_tags = []
1701
        new_tags = []
1702
1703
        to_remove = []
1704
        to_add = []
1705
1706
        tags_on_server = self.get_tags()
1707
1708
        for tag in tags_on_server:
1709
            c_old_tags.append(tag.get_name().lower())
1710
            old_tags.append(tag.get_name())
1711
1712
        for tag in tags:
1713
            c_new_tags.append(tag.lower())
1714
            new_tags.append(tag)
1715
1716
        for i in range(0, len(old_tags)):
1717
            if not c_old_tags[i] in c_new_tags:
1718
                to_remove.append(old_tags[i])
1719
1720
        for i in range(0, len(new_tags)):
1721
            if not c_new_tags[i] in c_old_tags:
1722
                to_add.append(new_tags[i])
1723
1724
        self.remove_tags(to_remove)
1725
        self.add_tags(to_add)
1726
1727 1
    def get_top_tags(self, limit=None):
1728
        """Returns a list of the most frequently used Tags on this object."""
1729
1730
        doc = self._request(self.ws_prefix + '.getTopTags', True)
1731
1732
        elements = doc.getElementsByTagName('tag')
1733
        seq = []
1734
1735
        for element in elements:
1736
            tag_name = _extract(element, 'name')
1737
            tagcount = _extract(element, 'count')
1738
1739
            seq.append(TopItem(Tag(tag_name, self.network), tagcount))
1740
1741
        if limit:
1742
            seq = seq[:limit]
1743
1744
        return seq
1745
1746
1747 1
class WSError(Exception):
1748
    """Exception related to the Network web service"""
1749
1750 1
    def __init__(self, network, status, details):
1751
        self.status = status
1752
        self.details = details
1753
        self.network = network
1754
1755 1
    @_string_output
1756
    def __str__(self):
1757
        return self.details
1758
1759 1
    def get_id(self):
1760
        """Returns the exception ID, from one of the following:
1761
            STATUS_INVALID_SERVICE = 2
1762
            STATUS_INVALID_METHOD = 3
1763
            STATUS_AUTH_FAILED = 4
1764
            STATUS_INVALID_FORMAT = 5
1765
            STATUS_INVALID_PARAMS = 6
1766
            STATUS_INVALID_RESOURCE = 7
1767
            STATUS_TOKEN_ERROR = 8
1768
            STATUS_INVALID_SK = 9
1769
            STATUS_INVALID_API_KEY = 10
1770
            STATUS_OFFLINE = 11
1771
            STATUS_SUBSCRIBERS_ONLY = 12
1772
            STATUS_TOKEN_UNAUTHORIZED = 14
1773
            STATUS_TOKEN_EXPIRED = 15
1774
        """
1775
1776
        return self.status
1777
1778
1779 1
class MalformedResponseError(Exception):
1780
    """Exception conveying a malformed response from the music network."""
1781
1782 1
    def __init__(self, network, underlying_error):
1783
        self.network = network
1784
        self.underlying_error = underlying_error
1785
1786 1
    def __str__(self):
1787
        return "Malformed response from {}. Underlying error: {}".format(
1788
            self.network.name, str(self.underlying_error))
1789
1790
1791 1
class NetworkError(Exception):
1792
    """Exception conveying a problem in sending a request to Last.fm"""
1793
1794 1
    def __init__(self, network, underlying_error):
1795
        self.network = network
1796
        self.underlying_error = underlying_error
1797
1798 1
    def __str__(self):
1799
        return "NetworkError: %s" % str(self.underlying_error)
1800
1801
1802 1
class _Opus(_BaseObject, _Taggable):
1803
    """An album or track."""
1804
1805 1
    artist = None
1806 1
    title = None
1807 1
    username = None
1808
1809 1
    __hash__ = _BaseObject.__hash__
1810
1811 1
    def __init__(self, artist, title, network, ws_prefix, username=None):
1812
        """
1813
        Create an opus instance.
1814
        # Parameters:
1815
            * artist: An artist name or an Artist object.
1816
            * title: The album or track title.
1817
            * ws_prefix: 'album' or 'track'
1818
        """
1819
1820
        _BaseObject.__init__(self, network, ws_prefix)
1821
        _Taggable.__init__(self, ws_prefix)
1822
1823
        if isinstance(artist, Artist):
1824
            self.artist = artist
1825
        else:
1826
            self.artist = Artist(artist, self.network)
1827
1828
        self.title = title
1829
        self.username = username
1830
1831 1
    def __repr__(self):
1832
        return "pylast.%s(%s, %s, %s)" % (
1833
            self.ws_prefix.title(), repr(self.artist.name),
1834
            repr(self.title), repr(self.network))
1835
1836 1
    @_string_output
1837
    def __str__(self):
1838
        return _unicode("%s - %s") % (
1839
            self.get_artist().get_name(), self.get_title())
1840
1841 1
    def __eq__(self, other):
1842
        if type(self) != type(other):
1843
            return False
1844
        a = self.get_title().lower()
1845
        b = other.get_title().lower()
1846
        c = self.get_artist().get_name().lower()
1847
        d = other.get_artist().get_name().lower()
1848
        return (a == b) and (c == d)
1849
1850 1
    def __ne__(self, other):
1851
        return not self.__eq__(other)
1852
1853 1
    def _get_params(self):
1854
        return {
1855
            'artist': self.get_artist().get_name(),
1856
            self.ws_prefix: self.get_title()}
1857
1858 1
    def get_artist(self):
1859
        """Returns the associated Artist object."""
1860
1861
        return self.artist
1862
1863 1
    def get_title(self, properly_capitalized=False):
1864
        """Returns the artist or track title."""
1865
        if properly_capitalized:
1866
            self.title = _extract(
1867
                self._request(self.ws_prefix + ".getInfo", True), "name")
1868
1869
        return self.title
1870
1871 1
    def get_name(self, properly_capitalized=False):
1872
        """Returns the album or track title (alias to get_title())."""
1873
1874
        return self.get_title(properly_capitalized)
1875
1876 1
    def get_id(self):
1877
        """Returns the ID on the network."""
1878
1879
        return _extract(
1880
            self._request(self.ws_prefix + ".getInfo", cacheable=True), "id")
1881
1882 1
    def get_playcount(self):
1883
        """Returns the number of plays on the network"""
1884
1885
        return _number(_extract(
1886
            self._request(
1887
                self.ws_prefix + ".getInfo", cacheable=True), "playcount"))
1888
1889 1
    def get_userplaycount(self):
1890
        """Returns the number of plays by a given username"""
1891
1892
        if not self.username:
1893
            return
1894
1895
        params = self._get_params()
1896
        params['username'] = self.username
1897
1898
        doc = self._request(self.ws_prefix + ".getInfo", True, params)
1899
        return _number(_extract(doc, "userplaycount"))
1900
1901 1
    def get_listener_count(self):
1902
        """Returns the number of listeners on the network"""
1903
1904
        return _number(_extract(
1905
            self._request(
1906
                self.ws_prefix + ".getInfo", cacheable=True), "listeners"))
1907
1908 1
    def get_mbid(self):
1909
        """Returns the MusicBrainz ID of the album or track."""
1910
1911
        doc = self._request(self.ws_prefix + ".getInfo", cacheable=True)
1912
1913
        try:
1914
            lfm = doc.getElementsByTagName('lfm')[0]
1915
            opus = next(self._get_children_by_tag_name(lfm, self.ws_prefix))
1916
            mbid = next(self._get_children_by_tag_name(opus, "mbid"))
1917
            return mbid.firstChild.nodeValue
1918
        except StopIteration:
1919
            return None
1920
1921 1
    def _get_children_by_tag_name(self, node, tag_name):
1922
        for child in node.childNodes:
1923
            if (child.nodeType == child.ELEMENT_NODE and
1924
               (tag_name == '*' or child.tagName == tag_name)):
1925
                yield child
1926
1927
1928 1
class Album(_Opus):
1929
    """An album."""
1930
1931 1
    __hash__ = _Opus.__hash__
1932
1933 1
    def __init__(self, artist, title, network, username=None):
1934
        super(Album, self).__init__(artist, title, network, "album", username)
1935
1936 1
    def get_release_date(self):
1937
        """Returns the release date of the album."""
1938
1939
        return _extract(self._request(
1940
            self.ws_prefix + ".getInfo", cacheable=True), "releasedate")
1941
1942 1
    def get_cover_image(self, size=COVER_EXTRA_LARGE):
1943
        """
1944
        Returns a uri to the cover image
1945
        size can be one of:
1946
            COVER_EXTRA_LARGE
1947
            COVER_LARGE
1948
            COVER_MEDIUM
1949
            COVER_SMALL
1950
        """
1951
1952
        return _extract_all(
1953
            self._request(
1954
                self.ws_prefix + ".getInfo", cacheable=True), 'image')[size]
1955
1956 1
    def get_tracks(self):
1957
        """Returns the list of Tracks on this album."""
1958
1959
        return _extract_tracks(
1960
            self._request(
1961
                self.ws_prefix + ".getInfo", cacheable=True), "tracks")
1962
1963 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
1964
        """Returns the URL of the album or track page on the network.
1965
        # Parameters:
1966
        * domain_name str: The network's language domain. Possible values:
1967
            o DOMAIN_ENGLISH
1968
            o DOMAIN_GERMAN
1969
            o DOMAIN_SPANISH
1970
            o DOMAIN_FRENCH
1971
            o DOMAIN_ITALIAN
1972
            o DOMAIN_POLISH
1973
            o DOMAIN_PORTUGUESE
1974
            o DOMAIN_SWEDISH
1975
            o DOMAIN_TURKISH
1976
            o DOMAIN_RUSSIAN
1977
            o DOMAIN_JAPANESE
1978
            o DOMAIN_CHINESE
1979
        """
1980
1981
        artist = _url_safe(self.get_artist().get_name())
1982
        title = _url_safe(self.get_title())
1983
1984
        return self.network._get_url(
1985
            domain_name, self.ws_prefix) % {
1986
            'artist': artist, 'album': title}
1987
1988
1989 1
class Artist(_BaseObject, _Taggable):
1990
    """An artist."""
1991
1992 1
    name = None
1993 1
    username = None
1994
1995 1
    __hash__ = _BaseObject.__hash__
1996
1997 1
    def __init__(self, name, network, username=None):
1998
        """Create an artist object.
1999
        # Parameters:
2000
            * name str: The artist's name.
2001
        """
2002
2003 1
        _BaseObject.__init__(self, network, 'artist')
2004 1
        _Taggable.__init__(self, 'artist')
2005
2006 1
        self.name = name
2007 1
        self.username = username
2008
2009 1
    def __repr__(self):
2010
        return "pylast.Artist(%s, %s)" % (
2011
            repr(self.get_name()), repr(self.network))
2012
2013 1
    def __unicode__(self):
2014 1
        return six.text_type(self.get_name())
2015
2016 1
    @_string_output
2017
    def __str__(self):
2018
        return self.__unicode__()
2019
2020 1
    def __eq__(self, other):
2021
        if type(self) is type(other):
2022
            return self.get_name().lower() == other.get_name().lower()
2023
        else:
2024
            return False
2025
2026 1
    def __ne__(self, other):
2027
        return not self.__eq__(other)
2028
2029 1
    def _get_params(self):
2030 1
        return {self.ws_prefix: self.get_name()}
2031
2032 1
    def get_name(self, properly_capitalized=False):
2033
        """Returns the name of the artist.
2034
        If properly_capitalized was asserted then the name would be downloaded
2035
        overwriting the given one."""
2036
2037 1
        if properly_capitalized:
2038
            self.name = _extract(
2039
                self._request(self.ws_prefix + ".getInfo", True), "name")
2040
2041 1
        return self.name
2042
2043 1
    def get_correction(self):
2044
        """Returns the corrected artist name."""
2045
2046
        return _extract(
2047
            self._request(self.ws_prefix + ".getCorrection"), "name")
2048
2049 1
    def get_cover_image(self, size=COVER_MEGA):
2050
        """
2051
        Returns a uri to the cover image
2052
        size can be one of:
2053
            COVER_MEGA
2054
            COVER_EXTRA_LARGE
2055
            COVER_LARGE
2056
            COVER_MEDIUM
2057
            COVER_SMALL
2058
        """
2059
2060
        return _extract_all(
2061
            self._request(self.ws_prefix + ".getInfo", True), "image")[size]
2062
2063 1
    def get_playcount(self):
2064
        """Returns the number of plays on the network."""
2065
2066
        return _number(_extract(
2067
            self._request(self.ws_prefix + ".getInfo", True), "playcount"))
2068
2069 1
    def get_userplaycount(self):
2070
        """Returns the number of plays by a given username"""
2071
2072
        if not self.username:
2073
            return
2074
2075
        params = self._get_params()
2076
        params['username'] = self.username
2077
2078
        doc = self._request(self.ws_prefix + ".getInfo", True, params)
2079
        return _number(_extract(doc, "userplaycount"))
2080
2081 1
    def get_mbid(self):
2082
        """Returns the MusicBrainz ID of this artist."""
2083
2084
        doc = self._request(self.ws_prefix + ".getInfo", True)
2085
2086
        return _extract(doc, "mbid")
2087
2088 1
    def get_listener_count(self):
2089
        """Returns the number of listeners on the network."""
2090
2091
        if hasattr(self, "listener_count"):
2092
            return self.listener_count
2093
        else:
2094
            self.listener_count = _number(_extract(
2095
                self._request(self.ws_prefix + ".getInfo", True), "listeners"))
2096
            return self.listener_count
2097
2098 1
    def is_streamable(self):
2099
        """Returns True if the artist is streamable."""
2100
2101
        return bool(_number(_extract(
2102
            self._request(self.ws_prefix + ".getInfo", True), "streamable")))
2103
2104 1
    def get_bio(self, section, language=None):
2105
        """
2106
        Returns a section of the bio.
2107
        section can be "content", "summary" or
2108
            "published" (for published date)
2109
        """
2110
        if language:
2111
            params = self._get_params()
2112
            params["lang"] = language
2113
        else:
2114
            params = None
2115
2116
        return self._extract_cdata_from_request(
2117
            self.ws_prefix + ".getInfo", section, params)
2118
2119 1
    def get_bio_published_date(self):
2120
        """Returns the date on which the artist's biography was published."""
2121
        return self.get_bio("published")
2122
2123 1
    def get_bio_summary(self, language=None):
2124
        """Returns the summary of the artist's biography."""
2125
        return self.get_bio("summary", language)
2126
2127 1
    def get_bio_content(self, language=None):
2128
        """Returns the content of the artist's biography."""
2129
        return self.get_bio("content", language)
2130
2131 1
    def get_upcoming_events(self):
2132
        """Returns a list of the upcoming Events for this artist."""
2133
2134
        doc = self._request(self.ws_prefix + '.getEvents', True)
2135
2136
        return _extract_events_from_doc(doc, self.network)
2137
2138 1
    def get_similar(self, limit=None):
2139
        """Returns the similar artists on the network."""
2140
2141
        params = self._get_params()
2142
        if limit:
2143
            params['limit'] = limit
2144
2145
        doc = self._request(self.ws_prefix + '.getSimilar', True, params)
2146
2147
        names = _extract_all(doc, "name")
2148
        matches = _extract_all(doc, "match")
2149
2150
        artists = []
2151
        for i in range(0, len(names)):
2152
            artists.append(SimilarItem(
2153
                Artist(names[i], self.network), _number(matches[i])))
2154
2155
        return artists
2156
2157 1
    def get_top_albums(self, limit=None, cacheable=True):
2158
        """Returns a list of the top albums."""
2159
        params = self._get_params()
2160
        if limit:
2161
            params['limit'] = limit
2162
2163
        return self._get_things(
2164
            "getTopAlbums", "album", Album, params, cacheable)
2165
2166 1
    def get_top_tracks(self, limit=None, cacheable=True):
2167
        """Returns a list of the most played Tracks by this artist."""
2168
        params = self._get_params()
2169
        if limit:
2170
            params['limit'] = limit
2171
2172
        return self._get_things(
2173
            "getTopTracks", "track", Track, params, cacheable)
2174
2175 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
2176
        """Returns the url of the artist page on the network.
2177
        # Parameters:
2178
        * domain_name: The network's language domain. Possible values:
2179
          o DOMAIN_ENGLISH
2180
          o DOMAIN_GERMAN
2181
          o DOMAIN_SPANISH
2182
          o DOMAIN_FRENCH
2183
          o DOMAIN_ITALIAN
2184
          o DOMAIN_POLISH
2185
          o DOMAIN_PORTUGUESE
2186
          o DOMAIN_SWEDISH
2187
          o DOMAIN_TURKISH
2188
          o DOMAIN_RUSSIAN
2189
          o DOMAIN_JAPANESE
2190
          o DOMAIN_CHINESE
2191
        """
2192
2193
        artist = _url_safe(self.get_name())
2194
2195
        return self.network._get_url(
2196
            domain_name, "artist") % {'artist': artist}
2197
2198 1
    def shout(self, message):
2199
        """
2200
            Post a shout
2201
        """
2202
2203
        params = self._get_params()
2204
        params["message"] = message
2205
2206
        self._request("artist.Shout", False, params)
2207
2208 1
    def get_band_members(self):
2209
        """Returns a list of band members or None if unknown."""
2210
2211
        names = None
2212
        doc = self._request(self.ws_prefix + ".getInfo", True)
2213
2214
        for node in doc.getElementsByTagName("bandmembers"):
2215
            names = _extract_all(node, "name")
2216
2217
        return names
2218
2219
2220 1
class Event(_BaseObject):
2221
    """An event."""
2222
2223 1
    id = None
2224
2225 1
    __hash__ = _BaseObject.__hash__
2226
2227 1
    def __init__(self, event_id, network):
2228
        _BaseObject.__init__(self, network, 'event')
2229
2230
        self.id = event_id
2231
2232 1
    def __repr__(self):
2233
        return "pylast.Event(%s, %s)" % (repr(self.id), repr(self.network))
2234
2235 1
    @_string_output
2236
    def __str__(self):
2237
        return "Event #" + str(self.get_id())
2238
2239 1
    def __eq__(self, other):
2240
        if type(self) is type(other):
2241
            return self.get_id() == other.get_id()
2242
        else:
2243
            return False
2244
2245 1
    def __ne__(self, other):
2246
        return not self.__eq__(other)
2247
2248 1
    def _get_params(self):
2249
        return {'event': self.get_id()}
2250
2251 1
    def attend(self, attending_status):
2252
        """Sets the attending status.
2253
        * attending_status: The attending status. Possible values:
2254
          o EVENT_ATTENDING
2255
          o EVENT_MAYBE_ATTENDING
2256
          o EVENT_NOT_ATTENDING
2257
        """
2258
2259
        params = self._get_params()
2260
        params['status'] = attending_status
2261
2262
        self._request('event.attend', False, params)
2263
2264 1
    def get_attendees(self):
2265
        """
2266
            Get a list of attendees for an event
2267
        """
2268
2269
        doc = self._request("event.getAttendees", False)
2270
2271
        users = []
2272
        for name in _extract_all(doc, "name"):
2273
            users.append(User(name, self.network))
2274
2275
        return users
2276
2277 1
    def get_id(self):
2278
        """Returns the id of the event on the network. """
2279
2280
        return self.id
2281
2282 1
    def get_title(self):
2283
        """Returns the title of the event. """
2284
2285
        doc = self._request("event.getInfo", True)
2286
2287
        return _extract(doc, "title")
2288
2289 1
    def get_headliner(self):
2290
        """Returns the headliner of the event. """
2291
2292
        doc = self._request("event.getInfo", True)
2293
2294
        return Artist(_extract(doc, "headliner"), self.network)
2295
2296 1
    def get_artists(self):
2297
        """Returns a list of the participating Artists. """
2298
2299
        doc = self._request("event.getInfo", True)
2300
        names = _extract_all(doc, "artist")
2301
2302
        artists = []
2303
        for name in names:
2304
            artists.append(Artist(name, self.network))
2305
2306
        return artists
2307
2308 1
    def get_venue(self):
2309
        """Returns the venue where the event is held."""
2310
2311
        doc = self._request("event.getInfo", True)
2312
2313
        v = doc.getElementsByTagName("venue")[0]
2314
        venue_id = _number(_extract(v, "id"))
2315
2316
        return Venue(venue_id, self.network, venue_element=v)
2317
2318 1
    def get_start_date(self):
2319
        """Returns the date when the event starts."""
2320
2321
        doc = self._request("event.getInfo", True)
2322
2323
        return _extract(doc, "startDate")
2324
2325 1
    def get_description(self):
2326
        """Returns the description of the event. """
2327
2328
        doc = self._request("event.getInfo", True)
2329
2330
        return _extract(doc, "description")
2331
2332 1
    def get_cover_image(self, size=COVER_MEGA):
2333
        """
2334
        Returns a uri to the cover image
2335
        size can be one of:
2336
            COVER_MEGA
2337
            COVER_EXTRA_LARGE
2338
            COVER_LARGE
2339
            COVER_MEDIUM
2340
            COVER_SMALL
2341
        """
2342
2343
        doc = self._request("event.getInfo", True)
2344
2345
        return _extract_all(doc, "image")[size]
2346
2347 1
    def get_attendance_count(self):
2348
        """Returns the number of attending people. """
2349
2350
        doc = self._request("event.getInfo", True)
2351
2352
        return _number(_extract(doc, "attendance"))
2353
2354 1
    def get_review_count(self):
2355
        """Returns the number of available reviews for this event. """
2356
2357
        doc = self._request("event.getInfo", True)
2358
2359
        return _number(_extract(doc, "reviews"))
2360
2361 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
2362
        """Returns the url of the event page on the network.
2363
        * domain_name: The network's language domain. Possible values:
2364
          o DOMAIN_ENGLISH
2365
          o DOMAIN_GERMAN
2366
          o DOMAIN_SPANISH
2367
          o DOMAIN_FRENCH
2368
          o DOMAIN_ITALIAN
2369
          o DOMAIN_POLISH
2370
          o DOMAIN_PORTUGUESE
2371
          o DOMAIN_SWEDISH
2372
          o DOMAIN_TURKISH
2373
          o DOMAIN_RUSSIAN
2374
          o DOMAIN_JAPANESE
2375
          o DOMAIN_CHINESE
2376
        """
2377
2378
        return self.network._get_url(
2379
            domain_name, "event") % {'id': self.get_id()}
2380
2381 1
    def shout(self, message):
2382
        """
2383
            Post a shout
2384
        """
2385
2386
        params = self._get_params()
2387
        params["message"] = message
2388
2389
        self._request("event.Shout", False, params)
2390
2391
2392 1
class Country(_BaseObject):
2393
    """A country at Last.fm."""
2394
2395 1
    name = None
2396
2397 1
    __hash__ = _BaseObject.__hash__
2398
2399 1
    def __init__(self, name, network):
2400
        _BaseObject.__init__(self, network, "geo")
2401
2402
        self.name = name
2403
2404 1
    def __repr__(self):
2405
        return "pylast.Country(%s, %s)" % (repr(self.name), repr(self.network))
2406
2407 1
    @_string_output
2408
    def __str__(self):
2409
        return self.get_name()
2410
2411 1
    def __eq__(self, other):
2412
        return self.get_name().lower() == other.get_name().lower()
2413
2414 1
    def __ne__(self, other):
2415
        return self.get_name() != other.get_name()
2416
2417 1
    def _get_params(self):  # TODO can move to _BaseObject
2418
        return {'country': self.get_name()}
2419
2420 1
    def _get_name_from_code(self, alpha2code):
2421
        # TODO: Have this function lookup the alpha-2 code and return the
2422
        # country name.
2423
2424
        return alpha2code
2425
2426 1
    def get_name(self):
2427
        """Returns the country name. """
2428
2429 View Code Duplication
        return self.name
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
2430
2431 1
    def get_top_artists(self, limit=None, cacheable=True):
2432
        """Returns a sequence of the most played artists."""
2433
        params = self._get_params()
2434
        if limit:
2435
            params['limit'] = limit
2436
2437
        doc = self._request('geo.getTopArtists', cacheable, params)
2438
2439
        return _extract_top_artists(doc, self)
2440
2441 1
    def get_top_tracks(self, limit=None, cacheable=True):
2442
        """Returns a sequence of the most played tracks"""
2443
        params = self._get_params()
2444
        if limit:
2445
            params['limit'] = limit
2446
2447
        return self._get_things(
2448
            "getTopTracks", "track", Track, params, cacheable)
2449
2450 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
2451
        """Returns the url of the event page on the network.
2452
        * domain_name: The network's language domain. Possible values:
2453
          o DOMAIN_ENGLISH
2454
          o DOMAIN_GERMAN
2455
          o DOMAIN_SPANISH
2456
          o DOMAIN_FRENCH
2457
          o DOMAIN_ITALIAN
2458
          o DOMAIN_POLISH
2459
          o DOMAIN_PORTUGUESE
2460
          o DOMAIN_SWEDISH
2461
          o DOMAIN_TURKISH
2462
          o DOMAIN_RUSSIAN
2463
          o DOMAIN_JAPANESE
2464
          o DOMAIN_CHINESE
2465
        """
2466
2467
        country_name = _url_safe(self.get_name())
2468
2469
        return self.network._get_url(
2470
            domain_name, "country") % {'country_name': country_name}
2471
2472
2473 1
class Metro(_BaseObject):
2474
    """A metro at Last.fm."""
2475
2476 1
    name = None
2477 1
    country = None
2478
2479 1
    __hash__ = _BaseObject.__hash__
2480
2481 1
    def __init__(self, name, country, network):
2482
        _BaseObject.__init__(self, network, None)
2483
2484
        self.name = name
2485
        self.country = country
2486
2487 1
    def __repr__(self):
2488
        return "pylast.Metro(%s, %s, %s)" % (
2489
            repr(self.name), repr(self.country), repr(self.network))
2490
2491 1
    @_string_output
2492
    def __str__(self):
2493
        return self.get_name() + ", " + self.get_country()
2494
2495 1
    def __eq__(self, other):
2496
        return (self.get_name().lower() == other.get_name().lower() and
2497
                self.get_country().lower() == other.get_country().lower())
2498
2499 1
    def __ne__(self, other):
2500
        return (self.get_name() != other.get_name() or
2501
                self.get_country().lower() != other.get_country().lower())
2502
2503 1
    def _get_params(self):
2504
        return {'metro': self.get_name(), 'country': self.get_country()}
2505
2506 1
    def get_name(self):
2507
        """Returns the metro name."""
2508
2509
        return self.name
2510
2511 1
    def get_country(self):
2512
        """Returns the metro country."""
2513
2514
        return self.country
2515
2516 1
    def _get_chart(
2517
            self, method, tag="artist", limit=None, from_date=None,
2518
            to_date=None, cacheable=True):
2519
        """Internal helper for getting geo charts."""
2520
        params = self._get_params()
2521
        if limit:
2522
            params["limit"] = limit
2523
        if from_date and to_date:
2524
            params["from"] = from_date
2525
            params["to"] = to_date
2526
2527
        doc = self._request(method, cacheable, params)
2528
2529
        seq = []
2530
        for node in doc.getElementsByTagName(tag):
2531
            if tag == "artist":
2532
                item = Artist(_extract(node, "name"), self.network)
2533
            elif tag == "track":
2534
                title = _extract(node, "name")
2535
                artist = _extract_element_tree(node).get('artist')['name']
2536
                item = Track(artist, title, self.network)
2537
            else:
2538
                return None
2539
            weight = _number(_extract(node, "listeners"))
2540
            seq.append(TopItem(item, weight))
2541
2542
        return seq
2543
2544 1
    def get_artist_chart(
2545
            self, tag="artist", limit=None, from_date=None, to_date=None,
2546
            cacheable=True):
2547
        """Get a chart of artists for a metro.
2548
        Parameters:
2549
        from_date (Optional) : Beginning timestamp of the weekly range
2550
            requested
2551
        to_date (Optional) : Ending timestamp of the weekly range requested
2552
        limit (Optional) : The number of results to fetch per page.
2553
            Defaults to 50.
2554
        """
2555
        return self._get_chart(
2556
            "geo.getMetroArtistChart", tag=tag, limit=limit,
2557
            from_date=from_date, to_date=to_date, cacheable=cacheable)
2558
2559 1
    def get_hype_artist_chart(
2560
            self, tag="artist", limit=None, from_date=None, to_date=None,
2561
            cacheable=True):
2562
        """Get a chart of hyped (up and coming) artists for a metro.
2563
        Parameters:
2564
        from_date (Optional) : Beginning timestamp of the weekly range
2565
            requested
2566
        to_date (Optional) : Ending timestamp of the weekly range requested
2567
        limit (Optional) : The number of results to fetch per page.
2568
            Defaults to 50.
2569
        """
2570
        return self._get_chart(
2571
            "geo.getMetroHypeArtistChart", tag=tag, limit=limit,
2572
            from_date=from_date, to_date=to_date, cacheable=cacheable)
2573
2574 1
    def get_unique_artist_chart(
2575
            self, tag="artist", limit=None, from_date=None, to_date=None,
2576
            cacheable=True):
2577
        """Get a chart of the artists which make that metro unique.
2578
        Parameters:
2579
        from_date (Optional) : Beginning timestamp of the weekly range
2580
            requested
2581
        to_date (Optional) : Ending timestamp of the weekly range requested
2582
        limit (Optional) : The number of results to fetch per page.
2583
            Defaults to 50.
2584
        """
2585
        return self._get_chart(
2586
            "geo.getMetroUniqueArtistChart", tag=tag, limit=limit,
2587
            from_date=from_date, to_date=to_date, cacheable=cacheable)
2588
2589 1
    def get_track_chart(
2590
            self, tag="track", limit=None, from_date=None, to_date=None,
2591
            cacheable=True):
2592
        """Get a chart of tracks for a metro.
2593
        Parameters:
2594
        from_date (Optional) : Beginning timestamp of the weekly range
2595
            requested
2596
        to_date (Optional) : Ending timestamp of the weekly range requested
2597
        limit (Optional) : The number of results to fetch per page.
2598
            Defaults to 50.
2599
        """
2600
        return self._get_chart(
2601
            "geo.getMetroTrackChart", tag=tag, limit=limit,
2602
            from_date=from_date, to_date=to_date, cacheable=cacheable)
2603
2604 1
    def get_hype_track_chart(
2605
            self, tag="track", limit=None, from_date=None, to_date=None,
2606
            cacheable=True):
2607
        """Get a chart of tracks for a metro.
2608
        Parameters:
2609
        from_date (Optional) : Beginning timestamp of the weekly range
2610
            requested
2611
        to_date (Optional) : Ending timestamp of the weekly range requested
2612
        limit (Optional) : The number of results to fetch per page.
2613
            Defaults to 50.
2614
        """
2615
        return self._get_chart(
2616
            "geo.getMetroHypeTrackChart", tag=tag,
2617
            limit=limit, from_date=from_date, to_date=to_date,
2618
            cacheable=cacheable)
2619
2620 1
    def get_unique_track_chart(
2621
            self, tag="track", limit=None, from_date=None, to_date=None,
2622
            cacheable=True):
2623
        """Get a chart of tracks for a metro.
2624
        Parameters:
2625
        from_date (Optional) : Beginning timestamp of the weekly range
2626
            requested
2627
        to_date (Optional) : Ending timestamp of the weekly range requested
2628
        limit (Optional) : The number of results to fetch per page.
2629
            Defaults to 50.
2630
        """
2631
        return self._get_chart(
2632
            "geo.getMetroUniqueTrackChart", tag=tag, limit=limit,
2633
            from_date=from_date, to_date=to_date, cacheable=cacheable)
2634
2635
2636 1
class Library(_BaseObject):
2637
    """A user's Last.fm library."""
2638
2639 1
    user = None
2640
2641 1
    __hash__ = _BaseObject.__hash__
2642
2643 1
    def __init__(self, user, network):
2644
        _BaseObject.__init__(self, network, 'library')
2645
2646
        if isinstance(user, User):
2647
            self.user = user
2648
        else:
2649
            self.user = User(user, self.network)
2650
2651
        self._albums_index = 0
2652
        self._artists_index = 0
2653
        self._tracks_index = 0
2654
2655 1
    def __repr__(self):
2656
        return "pylast.Library(%s, %s)" % (repr(self.user), repr(self.network))
2657
2658 1
    @_string_output
2659
    def __str__(self):
2660
        return repr(self.get_user()) + "'s Library"
2661
2662 1
    def _get_params(self):
2663
        return {'user': self.user.get_name()}
2664
2665 1
    def get_user(self):
2666
        """Returns the user who owns this library."""
2667
2668
        return self.user
2669
2670 1
    def add_album(self, album):
2671
        """Add an album to this library."""
2672
2673
        params = self._get_params()
2674
        params["artist"] = album.get_artist().get_name()
2675
        params["album"] = album.get_name()
2676
2677
        self._request("library.addAlbum", False, params)
2678
2679 1
    def remove_album(self, album):
2680
        """Remove an album from this library."""
2681
2682 View Code Duplication
        params = self._get_params()
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
2683
        params["artist"] = album.get_artist().get_name()
2684
        params["album"] = album.get_name()
2685
2686
        self._request(self.ws_prefix + ".removeAlbum", False, params)
2687
2688 1
    def add_artist(self, artist):
2689
        """Add an artist to this library."""
2690
2691
        params = self._get_params()
2692
        if type(artist) == str:
2693
            params["artist"] = artist
2694
        else:
2695
            params["artist"] = artist.get_name()
2696
2697
        self._request(self.ws_prefix + ".addArtist", False, params)
2698
2699 1
    def remove_artist(self, artist):
2700
        """Remove an artist from this library."""
2701
2702
        params = self._get_params()
2703
        if type(artist) == str:
2704
            params["artist"] = artist
2705
        else:
2706
            params["artist"] = artist.get_name()
2707
2708
        self._request(self.ws_prefix + ".removeArtist", False, params)
2709
2710 1
    def add_track(self, track):
2711
        """Add a track to this library."""
2712
2713
        params = self._get_params()
2714
        params["track"] = track.get_title()
2715
2716
        self._request(self.ws_prefix + ".addTrack", False, params)
2717
2718 1 View Code Duplication
    def get_albums(self, artist=None, limit=50, cacheable=True):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
2719
        """
2720
        Returns a sequence of Album objects
2721
        If no artist is specified, it will return all, sorted by decreasing
2722
        play count.
2723
        If limit==None it will return all (may take a while)
2724
        """
2725
2726
        params = self._get_params()
2727
        if artist:
2728
            params["artist"] = artist
2729
2730
        seq = []
2731
        for node in _collect_nodes(
2732
                limit,
2733
                self,
2734
                self.ws_prefix + ".getAlbums",
2735
                cacheable,
2736
                params):
2737
            name = _extract(node, "name")
2738
            artist = _extract(node, "name", 1)
2739
            playcount = _number(_extract(node, "playcount"))
2740
            tagcount = _number(_extract(node, "tagcount"))
2741
2742
            seq.append(LibraryItem(
2743
                Album(artist, name, self.network), playcount, tagcount))
2744
2745
        return seq
2746
2747 1
    def get_artists(self, limit=50, cacheable=True):
2748
        """
2749
        Returns a sequence of Album objects
2750
        if limit==None it will return all (may take a while)
2751
        """
2752
2753
        seq = []
2754
        for node in _collect_nodes(
2755
                limit,
2756
                self,
2757
                self.ws_prefix + ".getArtists",
2758
                cacheable):
2759
            name = _extract(node, "name")
2760
2761
            playcount = _number(_extract(node, "playcount"))
2762
            tagcount = _number(_extract(node, "tagcount"))
2763
2764
            seq.append(LibraryItem(
2765
                Artist(name, self.network), playcount, tagcount))
2766
2767
        return seq
2768
2769 1 View Code Duplication
    def get_tracks(self, artist=None, album=None, limit=50, cacheable=True):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
2770
        """
2771
        Returns a sequence of Album objects
2772
        If limit==None it will return all (may take a while)
2773
        """
2774
2775
        params = self._get_params()
2776
        if artist:
2777
            params["artist"] = artist
2778
        if album:
2779
            params["album"] = album
2780
2781
        seq = []
2782
        for node in _collect_nodes(
2783
                limit,
2784
                self,
2785
                self.ws_prefix + ".getTracks",
2786
                cacheable,
2787
                params):
2788
            name = _extract(node, "name")
2789
            artist = _extract(node, "name", 1)
2790
            playcount = _number(_extract(node, "playcount"))
2791
            tagcount = _number(_extract(node, "tagcount"))
2792
2793
            seq.append(LibraryItem(
2794
                Track(artist, name, self.network), playcount, tagcount))
2795
2796
        return seq
2797
2798 1
    def remove_scrobble(self, artist, title, timestamp):
2799
        """Remove a scrobble from a user's Last.fm library. Parameters:
2800
            artist (Required) : The artist that composed the track
2801
            title (Required) : The name of the track
2802
            timestamp (Required) : The unix timestamp of the scrobble
2803
                                   that you wish to remove
2804
        """
2805
2806
        params = self._get_params()
2807
        params["artist"] = artist
2808
        params["track"] = title
2809
        params["timestamp"] = timestamp
2810
2811
        self._request(self.ws_prefix + ".removeScrobble", False, params)
2812
2813
2814 1
class Playlist(_BaseObject):
2815
    """A Last.fm user playlist."""
2816
2817 1
    id = None
2818 1
    user = None
2819
2820 1
    __hash__ = _BaseObject.__hash__
2821
2822 1
    def __init__(self, user, playlist_id, network):
2823
        _BaseObject.__init__(self, network, "playlist")
2824
2825
        if isinstance(user, User):
2826
            self.user = user
2827
        else:
2828
            self.user = User(user, self.network)
2829
2830
        self.id = playlist_id
2831
2832 1
    @_string_output
2833
    def __str__(self):
2834
        return repr(self.user) + "'s playlist # " + repr(self.id)
2835
2836 1
    def _get_info_node(self):
2837
        """
2838
        Returns the node from user.getPlaylists where this playlist's info is.
2839
        """
2840
2841
        doc = self._request("user.getPlaylists", True)
2842
2843
        for node in doc.getElementsByTagName("playlist"):
2844
            if _extract(node, "id") == str(self.get_id()):
2845
                return node
2846
2847 1
    def _get_params(self):
2848
        return {'user': self.user.get_name(), 'playlistID': self.get_id()}
2849
2850 1
    def get_id(self):
2851
        """Returns the playlist ID."""
2852
2853
        return self.id
2854
2855 1
    def get_user(self):
2856
        """Returns the owner user of this playlist."""
2857
2858
        return self.user
2859
2860 1
    def get_tracks(self):
2861
        """Returns a list of the tracks on this user playlist."""
2862
2863
        uri = _unicode('lastfm://playlist/%s') % self.get_id()
2864
2865
        return XSPF(uri, self.network).get_tracks()
2866
2867 1
    def add_track(self, track):
2868
        """Adds a Track to this Playlist."""
2869
2870
        params = self._get_params()
2871
        params['artist'] = track.get_artist().get_name()
2872
        params['track'] = track.get_title()
2873
2874
        self._request('playlist.addTrack', False, params)
2875
2876 1
    def get_title(self):
2877
        """Returns the title of this playlist."""
2878
2879
        return _extract(self._get_info_node(), "title")
2880
2881 1
    def get_creation_date(self):
2882
        """Returns the creation date of this playlist."""
2883
2884
        return _extract(self._get_info_node(), "date")
2885
2886 1
    def get_size(self):
2887
        """Returns the number of tracks in this playlist."""
2888
2889
        return _number(_extract(self._get_info_node(), "size"))
2890
2891 1
    def get_description(self):
2892
        """Returns the description of this playlist."""
2893
2894
        return _extract(self._get_info_node(), "description")
2895
2896 1
    def get_duration(self):
2897
        """Returns the duration of this playlist in milliseconds."""
2898
2899
        return _number(_extract(self._get_info_node(), "duration"))
2900
2901 1
    def is_streamable(self):
2902
        """
2903
        Returns True if the playlist is streamable.
2904
        For a playlist to be streamable, it needs at least 45 tracks by 15
2905
        different artists."""
2906
2907
        if _extract(self._get_info_node(), "streamable") == '1':
2908
            return True
2909
        else:
2910
            return False
2911
2912 1
    def has_track(self, track):
2913
        """Checks to see if track is already in the playlist.
2914
        * track: Any Track object.
2915
        """
2916
2917
        return track in self.get_tracks()
2918
2919 1
    def get_cover_image(self, size=COVER_EXTRA_LARGE):
2920
        """
2921
        Returns a uri to the cover image
2922
        size can be one of:
2923
            COVER_MEGA
2924
            COVER_EXTRA_LARGE
2925
            COVER_LARGE
2926
            COVER_MEDIUM
2927
            COVER_SMALL
2928
        """
2929
2930
        return _extract(self._get_info_node(), "image")[size]
2931
2932 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
2933
        """Returns the url of the playlist on the network.
2934
        * domain_name: The network's language domain. Possible values:
2935
          o DOMAIN_ENGLISH
2936
          o DOMAIN_GERMAN
2937
          o DOMAIN_SPANISH
2938
          o DOMAIN_FRENCH
2939
          o DOMAIN_ITALIAN
2940
          o DOMAIN_POLISH
2941
          o DOMAIN_PORTUGUESE
2942
          o DOMAIN_SWEDISH
2943
          o DOMAIN_TURKISH
2944
          o DOMAIN_RUSSIAN
2945
          o DOMAIN_JAPANESE
2946
          o DOMAIN_CHINESE
2947
        """
2948
2949
        english_url = _extract(self._get_info_node(), "url")
2950
        appendix = english_url[english_url.rfind("/") + 1:]
2951
2952
        return self.network._get_url(domain_name, "playlist") % {
2953
            'appendix': appendix, "user": self.get_user().get_name()}
2954
2955
2956 1
class Tag(_BaseObject, _Chartable):
2957
    """A Last.fm object tag."""
2958
2959 1
    name = None
2960
2961 1
    __hash__ = _BaseObject.__hash__
2962
2963 1
    def __init__(self, name, network):
2964
        _BaseObject.__init__(self, network, 'tag')
2965
        _Chartable.__init__(self, 'tag')
2966
2967
        self.name = name
2968
2969 1
    def __repr__(self):
2970
        return "pylast.Tag(%s, %s)" % (repr(self.name), repr(self.network))
2971
2972 1
    @_string_output
2973
    def __str__(self):
2974
        return self.get_name()
2975
2976 1
    def __eq__(self, other):
2977
        return self.get_name().lower() == other.get_name().lower()
2978
2979 1
    def __ne__(self, other):
2980
        return self.get_name().lower() != other.get_name().lower()
2981
2982 1
    def _get_params(self):
2983
        return {self.ws_prefix: self.get_name()}
2984
2985 1
    def get_name(self, properly_capitalized=False):
2986
        """Returns the name of the tag. """
2987
2988
        if properly_capitalized:
2989
            self.name = _extract(
2990
                self._request(self.ws_prefix + ".getInfo", True), "name")
2991
2992
        return self.name
2993
2994 1
    def get_similar(self):
2995
        """Returns the tags similar to this one, ordered by similarity. """
2996
2997
        doc = self._request(self.ws_prefix + '.getSimilar', True)
2998
2999
        seq = []
3000
        names = _extract_all(doc, 'name')
3001
        for name in names:
3002
            seq.append(Tag(name, self.network))
3003
3004
        return seq
3005
3006 1
    def get_top_albums(self, limit=None, cacheable=True):
3007
        """Retuns a list of the top albums."""
3008
        params = self._get_params()
3009
        if limit:
3010
            params['limit'] = limit
3011
3012
        doc = self._request(
3013
            self.ws_prefix + '.getTopAlbums', cacheable, params)
3014
3015
        return _extract_top_albums(doc, self.network)
3016
3017 1
    def get_top_tracks(self, limit=None, cacheable=True):
3018
        """Returns a list of the most played Tracks for this tag."""
3019
        params = self._get_params()
3020
        if limit:
3021
            params['limit'] = limit
3022
3023
        return self._get_things(
3024
            "getTopTracks", "track", Track, params, cacheable)
3025
3026 1
    def get_top_artists(self, limit=None, cacheable=True):
3027
        """Returns a sequence of the most played artists."""
3028
3029
        params = self._get_params()
3030
        if limit:
3031
            params['limit'] = limit
3032
3033
        doc = self._request(
3034
            self.ws_prefix + '.getTopArtists', cacheable, params)
3035
3036
        return _extract_top_artists(doc, self.network)
3037
3038 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
3039
        """Returns the url of the tag page on the network.
3040
        * domain_name: The network's language domain. Possible values:
3041
          o DOMAIN_ENGLISH
3042
          o DOMAIN_GERMAN
3043
          o DOMAIN_SPANISH
3044
          o DOMAIN_FRENCH
3045
          o DOMAIN_ITALIAN
3046
          o DOMAIN_POLISH
3047
          o DOMAIN_PORTUGUESE
3048
          o DOMAIN_SWEDISH
3049
          o DOMAIN_TURKISH
3050
          o DOMAIN_RUSSIAN
3051
          o DOMAIN_JAPANESE
3052
          o DOMAIN_CHINESE
3053
        """
3054
3055
        name = _url_safe(self.get_name())
3056
3057
        return self.network._get_url(domain_name, "tag") % {'name': name}
3058
3059
3060 1
class Track(_Opus):
3061
    """A Last.fm track."""
3062
3063 1
    __hash__ = _Opus.__hash__
3064
3065 1
    def __init__(self, artist, title, network, username=None):
3066
        super(Track, self).__init__(artist, title, network, "track", username)
3067
3068 1
    def get_correction(self):
3069
        """Returns the corrected track name."""
3070
3071
        return _extract(
3072
            self._request(self.ws_prefix + ".getCorrection"), "name")
3073
3074 1
    def get_duration(self):
3075
        """Returns the track duration."""
3076
3077
        doc = self._request(self.ws_prefix + ".getInfo", True)
3078
3079
        return _number(_extract(doc, "duration"))
3080
3081 1
    def get_userloved(self):
3082
        """Whether the user loved this track"""
3083
3084
        if not self.username:
3085
            return
3086
3087
        params = self._get_params()
3088
        params['username'] = self.username
3089
3090
        doc = self._request(self.ws_prefix + ".getInfo", True, params)
3091
        loved = _number(_extract(doc, "userloved"))
3092
        return bool(loved)
3093
3094 1
    def is_streamable(self):
3095
        """Returns True if the track is available at Last.fm."""
3096
3097
        doc = self._request(self.ws_prefix + ".getInfo", True)
3098
        return _extract(doc, "streamable") == "1"
3099
3100 1
    def is_fulltrack_available(self):
3101
        """Returns True if the fulltrack is available for streaming."""
3102
3103
        doc = self._request(self.ws_prefix + ".getInfo", True)
3104
        return doc.getElementsByTagName(
3105
            "streamable")[0].getAttribute("fulltrack") == "1"
3106
3107 1
    def get_album(self):
3108
        """Returns the album object of this track."""
3109
3110
        doc = self._request(self.ws_prefix + ".getInfo", True)
3111
3112
        albums = doc.getElementsByTagName("album")
3113
3114
        if len(albums) == 0:
3115
            return
3116
3117
        node = doc.getElementsByTagName("album")[0]
3118
        return Album(
3119
            _extract(node, "artist"), _extract(node, "title"), self.network)
3120
3121 1
    def love(self):
3122
        """Adds the track to the user's loved tracks. """
3123
3124
        self._request(self.ws_prefix + '.love')
3125
3126 1
    def unlove(self):
3127
        """Remove the track to the user's loved tracks. """
3128
3129
        self._request(self.ws_prefix + '.unlove')
3130
3131 1
    def ban(self):
3132
        """Ban this track from ever playing on the radio. """
3133
3134
        self._request(self.ws_prefix + '.ban')
3135
3136 1
    def get_similar(self):
3137
        """
3138
        Returns similar tracks for this track on the network,
3139
        based on listening data.
3140
        """
3141
3142
        doc = self._request(self.ws_prefix + '.getSimilar', True)
3143
3144
        seq = []
3145
        for node in doc.getElementsByTagName(self.ws_prefix):
3146
            title = _extract(node, 'name')
3147
            artist = _extract(node, 'name', 1)
3148
            match = _number(_extract(node, "match"))
3149
3150
            seq.append(SimilarItem(Track(artist, title, self.network), match))
3151
3152
        return seq
3153
3154 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
3155
        """Returns the URL of the album or track page on the network.
3156
        # Parameters:
3157
        * domain_name str: The network's language domain. Possible values:
3158
            o DOMAIN_ENGLISH
3159
            o DOMAIN_GERMAN
3160
            o DOMAIN_SPANISH
3161
            o DOMAIN_FRENCH
3162
            o DOMAIN_ITALIAN
3163
            o DOMAIN_POLISH
3164
            o DOMAIN_PORTUGUESE
3165
            o DOMAIN_SWEDISH
3166
            o DOMAIN_TURKISH
3167
            o DOMAIN_RUSSIAN
3168
            o DOMAIN_JAPANESE
3169
            o DOMAIN_CHINESE
3170
        """
3171
3172
        artist = _url_safe(self.get_artist().get_name())
3173
        title = _url_safe(self.get_title())
3174
3175
        return self.network._get_url(
3176
            domain_name, self.ws_prefix) % {
3177
            'artist': artist, 'title': title}
3178
3179
3180 1
class Group(_BaseObject, _Chartable):
3181
    """A Last.fm group."""
3182
3183 1
    name = None
3184
3185 1
    __hash__ = _BaseObject.__hash__
3186
3187 1
    def __init__(self, name, network):
3188
        _BaseObject.__init__(self, network, 'group')
3189
        _Chartable.__init__(self, 'group')
3190
3191
        self.name = name
3192
3193 1
    def __repr__(self):
3194
        return "pylast.Group(%s, %s)" % (repr(self.name), repr(self.network))
3195
3196 1
    @_string_output
3197
    def __str__(self):
3198
        return self.get_name()
3199
3200 1
    def __eq__(self, other):
3201
        return self.get_name().lower() == other.get_name().lower()
3202
3203 1
    def __ne__(self, other):
3204
        return self.get_name() != other.get_name()
3205
3206 1
    def _get_params(self):
3207
        return {self.ws_prefix: self.get_name()}
3208
3209 1
    def get_name(self):
3210
        """Returns the group name. """
3211
        return self.name
3212
3213 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
3214
        """Returns the url of the group page on the network.
3215
        * domain_name: The network's language domain. Possible values:
3216
          o DOMAIN_ENGLISH
3217
          o DOMAIN_GERMAN
3218
          o DOMAIN_SPANISH
3219
          o DOMAIN_FRENCH
3220
          o DOMAIN_ITALIAN
3221
          o DOMAIN_POLISH
3222
          o DOMAIN_PORTUGUESE
3223
          o DOMAIN_SWEDISH
3224
          o DOMAIN_TURKISH
3225
          o DOMAIN_RUSSIAN
3226
          o DOMAIN_JAPANESE
3227
          o DOMAIN_CHINESE
3228
        """
3229
3230
        name = _url_safe(self.get_name())
3231
3232
        return self.network._get_url(domain_name, "group") % {'name': name}
3233
3234 1
    def get_members(self, limit=50, cacheable=False):
3235
        """
3236
            Returns a sequence of User objects
3237
            if limit==None it will return all
3238
        """
3239
3240
        nodes = _collect_nodes(
3241
            limit, self, self.ws_prefix + ".getMembers", cacheable)
3242
3243
        users = []
3244
3245
        for node in nodes:
3246
            users.append(User(_extract(node, "name"), self.network))
3247
3248
        return users
3249
3250
3251 1
class XSPF(_BaseObject):
3252
    "A Last.fm XSPF playlist."""
3253
3254 1
    uri = None
3255
3256 1
    __hash__ = _BaseObject.__hash__
3257
3258 1
    def __init__(self, uri, network):
3259
        _BaseObject.__init__(self, network, None)
3260
3261
        self.uri = uri
3262
3263 1
    def _get_params(self):
3264 View Code Duplication
        return {'playlistURL': self.get_uri()}
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
3265
3266 1
    @_string_output
3267
    def __str__(self):
3268
        return self.get_uri()
3269
3270 1
    def __eq__(self, other):
3271
        return self.get_uri() == other.get_uri()
3272
3273 1
    def __ne__(self, other):
3274
        return self.get_uri() != other.get_uri()
3275
3276 1
    def get_uri(self):
3277
        """Returns the Last.fm playlist URI. """
3278
3279
        return self.uri
3280
3281 1
    def get_tracks(self):
3282
        """Returns the tracks on this playlist."""
3283
3284
        doc = self._request('playlist.fetch', True)
3285
3286
        seq = []
3287
        for node in doc.getElementsByTagName('track'):
3288
            title = _extract(node, 'title')
3289
            artist = _extract(node, 'creator')
3290
3291
            seq.append(Track(artist, title, self.network))
3292
3293
        return seq
3294
3295
3296 1
class User(_BaseObject, _Chartable):
3297
    """A Last.fm user."""
3298
3299 1
    name = None
3300
3301 1
    __hash__ = _BaseObject.__hash__
3302
3303 1
    def __init__(self, user_name, network):
3304
        _BaseObject.__init__(self, network, 'user')
3305
        _Chartable.__init__(self, 'user')
3306
3307 View Code Duplication
        self.name = user_name
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
3308
3309
        self._past_events_index = 0
3310
        self._recommended_events_index = 0
3311
        self._recommended_artists_index = 0
3312
3313 1
    def __repr__(self):
3314
        return "pylast.User(%s, %s)" % (repr(self.name), repr(self.network))
3315
3316 1
    @_string_output
3317
    def __str__(self):
3318
        return self.get_name()
3319
3320 1
    def __eq__(self, another):
3321
        if isinstance(another, User):
3322
            return self.get_name() == another.get_name()
3323
        else:
3324
            return False
3325
3326 1
    def __ne__(self, another):
3327
        if isinstance(another, User):
3328
            return self.get_name() != another.get_name()
3329
        else:
3330
            return True
3331
3332 1
    def _get_params(self):
3333
        return {self.ws_prefix: self.get_name()}
3334
3335 1
    def get_name(self, properly_capitalized=False):
3336
        """Returns the user name."""
3337
3338
        if properly_capitalized:
3339
            self.name = _extract(
3340
                self._request(self.ws_prefix + ".getInfo", True), "name")
3341
3342
        return self.name
3343
3344 1
    def get_upcoming_events(self):
3345
        """Returns all the upcoming events for this user."""
3346
3347
        doc = self._request(self.ws_prefix + '.getEvents', True)
3348
3349
        return _extract_events_from_doc(doc, self.network)
3350
3351 1
    def get_artist_tracks(self, artist, cacheable=False):
3352
        """
3353
        Get a list of tracks by a given artist scrobbled by this user,
3354
        including scrobble time.
3355
        """
3356
        # Not implemented:
3357
        # "Can be limited to specific timeranges, defaults to all time."
3358
3359
        params = self._get_params()
3360
        params['artist'] = artist
3361
3362
        seq = []
3363
        for track in _collect_nodes(
3364
                None,
3365
                self,
3366
                self.ws_prefix + ".getArtistTracks",
3367
                cacheable,
3368
                params):
3369
            title = _extract(track, "name")
3370
            artist = _extract(track, "artist")
3371
            date = _extract(track, "date")
3372
            album = _extract(track, "album")
3373
            timestamp = track.getElementsByTagName(
3374
                "date")[0].getAttribute("uts")
3375
3376
            seq.append(PlayedTrack(
3377
                Track(artist, title, self.network), album, date, timestamp))
3378
3379
        return seq
3380
3381 1
    def get_friends(self, limit=50, cacheable=False):
3382
        """Returns a list of the user's friends. """
3383
3384
        seq = []
3385
        for node in _collect_nodes(
3386
                limit,
3387
                self,
3388
                self.ws_prefix + ".getFriends",
3389
                cacheable):
3390
            seq.append(User(_extract(node, "name"), self.network))
3391
3392
        return seq
3393
3394 1
    def get_loved_tracks(self, limit=50, cacheable=True):
3395
        """
3396
        Returns this user's loved track as a sequence of LovedTrack objects in
3397
        reverse order of their timestamp, all the way back to the first track.
3398
3399
        If limit==None, it will try to pull all the available data.
3400
3401
        This method uses caching. Enable caching only if you're pulling a
3402
        large amount of data.
3403
3404
        Use extract_items() with the return of this function to
3405
        get only a sequence of Track objects with no playback dates.
3406
        """
3407
3408
        params = self._get_params()
3409
        if limit:
3410
            params['limit'] = limit
3411
3412
        seq = []
3413
        for track in _collect_nodes(
3414 View Code Duplication
                limit,
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
3415
                self,
3416
                self.ws_prefix + ".getLovedTracks",
3417
                cacheable,
3418
                params):
3419
            title = _extract(track, "name")
3420
            artist = _extract(track, "name", 1)
3421
            date = _extract(track, "date")
3422
            timestamp = track.getElementsByTagName(
3423
                "date")[0].getAttribute("uts")
3424
3425
            seq.append(LovedTrack(
3426
                Track(artist, title, self.network), date, timestamp))
3427
3428
        return seq
3429
3430 1
    def get_neighbours(self, limit=50, cacheable=True):
3431
        """Returns a list of the user's friends."""
3432
3433
        params = self._get_params()
3434
        if limit:
3435
            params['limit'] = limit
3436
3437
        doc = self._request(
3438
            self.ws_prefix + '.getNeighbours', cacheable, params)
3439
3440
        seq = []
3441
        names = _extract_all(doc, 'name')
3442
3443
        for name in names:
3444
            seq.append(User(name, self.network))
3445
3446
        return seq
3447
3448 1
    def get_past_events(self, limit=50, cacheable=False):
3449
        """
3450
        Returns a sequence of Event objects
3451
        if limit==None it will return all
3452
        """
3453
3454
        seq = []
3455
        for node in _collect_nodes(
3456
                limit,
3457
                self,
3458
                self.ws_prefix + ".getPastEvents",
3459
                cacheable):
3460
            seq.append(Event(_extract(node, "id"), self.network))
3461
3462
        return seq
3463
3464 1
    def get_playlists(self):
3465
        """Returns a list of Playlists that this user owns."""
3466
3467
        doc = self._request(self.ws_prefix + ".getPlaylists", True)
3468
3469
        playlists = []
3470
        for playlist_id in _extract_all(doc, "id"):
3471
            playlists.append(
3472
                Playlist(self.get_name(), playlist_id, self.network))
3473
3474
        return playlists
3475
3476 1
    def get_now_playing(self):
3477
        """
3478
        Returns the currently playing track, or None if nothing is playing.
3479
        """
3480
3481
        params = self._get_params()
3482
        params['limit'] = '1'
3483
3484
        doc = self._request(self.ws_prefix + '.getRecentTracks', False, params)
3485
3486
        tracks = doc.getElementsByTagName('track')
3487
3488
        if len(tracks) == 0:
3489
            return None
3490
3491
        e = tracks[0]
3492
3493
        if not e.hasAttribute('nowplaying'):
3494
            return None
3495
3496
        artist = _extract(e, 'artist')
3497
        title = _extract(e, 'name')
3498
3499
        return Track(artist, title, self.network, self.name)
3500
3501 1
    def get_recent_tracks(self, limit=10, cacheable=True,
3502
                          time_from=None, time_to=None):
3503
        """
3504
        Returns this user's played track as a sequence of PlayedTrack objects
3505
        in reverse order of playtime, all the way back to the first track.
3506
3507
        Parameters:
3508
        limit : If None, it will try to pull all the available data.
3509
        from (Optional) : Beginning timestamp of a range - only display
3510
        scrobbles after this time, in UNIX timestamp format (integer
3511
        number of seconds since 00:00:00, January 1st 1970 UTC). This
3512
        must be in the UTC time zone.
3513
        to (Optional) : End timestamp of a range - only display scrobbles
3514
        before this time, in UNIX timestamp format (integer number of
3515
        seconds since 00:00:00, January 1st 1970 UTC). This must be in
3516
        the UTC time zone.
3517
3518
        This method uses caching. Enable caching only if you're pulling a
3519
        large amount of data.
3520
3521
        Use extract_items() with the return of this function to
3522
        get only a sequence of Track objects with no playback dates.
3523
        """
3524
3525
        params = self._get_params()
3526
        if limit:
3527
            params['limit'] = limit
3528
        if time_from:
3529
            params['from'] = time_from
3530
        if time_to:
3531
            params['to'] = time_to
3532
3533
        seq = []
3534
        for track in _collect_nodes(
3535
                limit,
3536
                self,
3537
                self.ws_prefix + ".getRecentTracks",
3538
                cacheable,
3539
                params):
3540
3541
            if track.hasAttribute('nowplaying'):
3542
                continue  # to prevent the now playing track from sneaking in
3543
3544
            title = _extract(track, "name")
3545
            artist = _extract(track, "artist")
3546
            date = _extract(track, "date")
3547
            album = _extract(track, "album")
3548
            timestamp = track.getElementsByTagName(
3549
                "date")[0].getAttribute("uts")
3550
3551
            seq.append(PlayedTrack(
3552
                Track(artist, title, self.network), album, date, timestamp))
3553
3554
        return seq
3555
3556 1
    def get_id(self):
3557
        """Returns the user ID."""
3558
3559
        doc = self._request(self.ws_prefix + ".getInfo", True)
3560
3561
        return _extract(doc, "id")
3562
3563 1
    def get_language(self):
3564
        """Returns the language code of the language used by the user."""
3565
3566
        doc = self._request(self.ws_prefix + ".getInfo", True)
3567
3568
        return _extract(doc, "lang")
3569
3570 1
    def get_country(self):
3571
        """Returns the name of the country of the user."""
3572
3573
        doc = self._request(self.ws_prefix + ".getInfo", True)
3574
3575
        country = _extract(doc, "country")
3576
3577
        if country is None:
3578
            return None
3579
        else:
3580
            return Country(country, self.network)
3581
3582 1
    def get_age(self):
3583
        """Returns the user's age."""
3584
3585
        doc = self._request(self.ws_prefix + ".getInfo", True)
3586
3587
        return _number(_extract(doc, "age"))
3588
3589 1
    def get_gender(self):
3590
        """Returns the user's gender. Either USER_MALE or USER_FEMALE."""
3591
3592
        doc = self._request(self.ws_prefix + ".getInfo", True)
3593
3594
        value = _extract(doc, "gender")
3595
3596
        if value == 'm':
3597
            return USER_MALE
3598
        elif value == 'f':
3599
            return USER_FEMALE
3600
3601
        return None
3602
3603 1
    def is_subscriber(self):
3604
        """Returns whether the user is a subscriber or not. True or False."""
3605
3606
        doc = self._request(self.ws_prefix + ".getInfo", True)
3607
3608
        return _extract(doc, "subscriber") == "1"
3609
3610 1
    def get_playcount(self):
3611
        """Returns the user's playcount so far."""
3612
3613
        doc = self._request(self.ws_prefix + ".getInfo", True)
3614
3615
        return _number(_extract(doc, "playcount"))
3616
3617 1
    def get_registered(self):
3618
        """Returns the user's registration date."""
3619
3620
        doc = self._request(self.ws_prefix + ".getInfo", True)
3621
3622
        return _extract(doc, "registered")
3623
3624 1
    def get_unixtime_registered(self):
3625
        """Returns the user's registration date as a UNIX timestamp."""
3626
3627
        doc = self._request(self.ws_prefix + ".getInfo", True)
3628
3629
        return doc.getElementsByTagName(
3630
            "registered")[0].getAttribute("unixtime")
3631
3632 1
    def get_tagged_albums(self, tag, limit=None, cacheable=True):
3633
        """Returns the albums tagged by a user."""
3634
3635
        params = self._get_params()
3636
        params['tag'] = tag
3637
        params['taggingtype'] = 'album'
3638
        if limit:
3639
            params['limit'] = limit
3640
        doc = self._request(self.ws_prefix + '.getpersonaltags', cacheable,
3641
                            params)
3642
        return _extract_albums(doc, self.network)
3643
3644 1
    def get_tagged_artists(self, tag, limit=None):
3645
        """Returns the artists tagged by a user."""
3646
3647
        params = self._get_params()
3648
        params['tag'] = tag
3649
        params['taggingtype'] = 'artist'
3650
        if limit:
3651
            params["limit"] = limit
3652
        doc = self._request(self.ws_prefix + '.getpersonaltags', True, params)
3653
        return _extract_artists(doc, self.network)
3654
3655 1
    def get_tagged_tracks(self, tag, limit=None, cacheable=True):
3656
        """Returns the tracks tagged by a user."""
3657
3658
        params = self._get_params()
3659
        params['tag'] = tag
3660
        params['taggingtype'] = 'track'
3661
        if limit:
3662
            params['limit'] = limit
3663
        doc = self._request(self.ws_prefix + '.getpersonaltags', cacheable,
3664
                            params)
3665
        return _extract_tracks(doc, self.network)
3666
3667 1
    def get_top_albums(
3668
            self, period=PERIOD_OVERALL, limit=None, cacheable=True):
3669
        """Returns the top albums played by a user.
3670
        * period: The period of time. Possible values:
3671
          o PERIOD_OVERALL
3672
          o PERIOD_7DAYS
3673
          o PERIOD_1MONTH
3674
          o PERIOD_3MONTHS
3675
          o PERIOD_6MONTHS
3676
          o PERIOD_12MONTHS
3677
        """
3678
3679
        params = self._get_params()
3680
        params['period'] = period
3681
        if limit:
3682
            params['limit'] = limit
3683
3684
        doc = self._request(
3685
            self.ws_prefix + '.getTopAlbums', cacheable, params)
3686
3687
        return _extract_top_albums(doc, self.network)
3688
3689 1
    def get_top_artists(self, period=PERIOD_OVERALL, limit=None):
3690
        """Returns the top artists played by a user.
3691
        * period: The period of time. Possible values:
3692
          o PERIOD_OVERALL
3693
          o PERIOD_7DAYS
3694
          o PERIOD_1MONTH
3695
          o PERIOD_3MONTHS
3696
          o PERIOD_6MONTHS
3697
          o PERIOD_12MONTHS
3698
        """
3699
3700
        params = self._get_params()
3701
        params['period'] = period
3702
        if limit:
3703
            params["limit"] = limit
3704
3705
        doc = self._request(self.ws_prefix + '.getTopArtists', True, params)
3706
3707
        return _extract_top_artists(doc, self.network)
3708
3709 1
    def get_top_tags(self, limit=None, cacheable=True):
3710
        """
3711
        Returns a sequence of the top tags used by this user with their counts
3712
        as TopItem objects.
3713
        * limit: The limit of how many tags to return.
3714
        * cacheable: Whether to cache results.
3715
        """
3716
3717
        params = self._get_params()
3718
        if limit:
3719
            params["limit"] = limit
3720
3721
        doc = self._request(self.ws_prefix + ".getTopTags", cacheable, params)
3722
3723
        seq = []
3724
        for node in doc.getElementsByTagName("tag"):
3725
            seq.append(TopItem(
3726
                Tag(_extract(node, "name"), self.network),
3727
                _extract(node, "count")))
3728
3729
        return seq
3730
3731 1
    def get_top_tracks(
3732
            self, period=PERIOD_OVERALL, limit=None, cacheable=True):
3733
        """Returns the top tracks played by a user.
3734
        * period: The period of time. Possible values:
3735
          o PERIOD_OVERALL
3736
          o PERIOD_7DAYS
3737
          o PERIOD_1MONTH
3738
          o PERIOD_3MONTHS
3739
          o PERIOD_6MONTHS
3740
          o PERIOD_12MONTHS
3741
        """
3742
3743
        params = self._get_params()
3744
        params['period'] = period
3745
        if limit:
3746
            params['limit'] = limit
3747
3748
        return self._get_things(
3749
            "getTopTracks", "track", Track, params, cacheable)
3750
3751 1
    def compare_with_user(self, user, shared_artists_limit=None):
3752
        """
3753
        Compare this user with another Last.fm user.
3754
        Returns a sequence:
3755
            (tasteometer_score, (shared_artist1, shared_artist2, ...))
3756
        user: A User object or a username string/unicode object.
3757
        """
3758
3759
        if isinstance(user, User):
3760
            user = user.get_name()
3761
3762
        params = self._get_params()
3763
        if shared_artists_limit:
3764
            params['limit'] = shared_artists_limit
3765
        params['type1'] = 'user'
3766
        params['type2'] = 'user'
3767
        params['value1'] = self.get_name()
3768
        params['value2'] = user
3769
3770
        doc = self._request('tasteometer.compare', False, params)
3771
3772
        score = _extract(doc, 'score')
3773
3774
        artists = doc.getElementsByTagName('artists')[0]
3775
        shared_artists_names = _extract_all(artists, 'name')
3776
3777
        shared_artists_seq = []
3778
3779
        for name in shared_artists_names:
3780
            shared_artists_seq.append(Artist(name, self.network))
3781
3782
        return (score, shared_artists_seq)
3783
3784 1
    def get_image(self):
3785
        """Returns the user's avatar."""
3786
3787
        doc = self._request(self.ws_prefix + ".getInfo", True)
3788
3789
        return _extract(doc, "image")
3790
3791 1
    def get_url(self, domain_name=DOMAIN_ENGLISH):
3792
        """Returns the url of the user page on the network.
3793
        * domain_name: The network's language domain. Possible values:
3794
          o DOMAIN_ENGLISH
3795
          o DOMAIN_GERMAN
3796
          o DOMAIN_SPANISH
3797
          o DOMAIN_FRENCH
3798
          o DOMAIN_ITALIAN
3799
          o DOMAIN_POLISH
3800
          o DOMAIN_PORTUGUESE
3801
          o DOMAIN_SWEDISH
3802
          o DOMAIN_TURKISH
3803
          o DOMAIN_RUSSIAN
3804
          o DOMAIN_JAPANESE
3805
          o DOMAIN_CHINESE
3806
        """
3807
3808
        name = _url_safe(self.get_name())
3809
3810
        return self.network._get_url(domain_name, "user") % {'name': name}
3811
3812 1
    def get_library(self):
3813
        """Returns the associated Library object. """
3814
3815
        return Library(self, self.network)
3816
3817 1
    def shout(self, message):
3818
        """
3819
            Post a shout
3820
        """
3821
3822
        params = self._get_params()
3823
        params["message"] = message
3824
3825
        self._request(self.ws_prefix + ".Shout", False, params)
3826
3827
3828 1
class AuthenticatedUser(User):
3829 1
    def __init__(self, network):
3830
        User.__init__(self, "", network)
3831
3832 1
    def _get_params(self):
3833
        return {"user": self.get_name()}
3834
3835 1
    def get_name(self):
3836
        """Returns the name of the authenticated user."""
3837
3838
        doc = self._request("user.getInfo", True, {"user": ""})    # hack
3839
3840
        self.name = _extract(doc, "name")
3841
        return self.name
3842
3843 1
    def get_recommended_events(self, limit=50, cacheable=False):
3844
        """
3845
        Returns a sequence of Event objects
3846
        if limit==None it will return all
3847
        """
3848
3849
        seq = []
3850
        for node in _collect_nodes(
3851
                limit, self, "user.getRecommendedEvents", cacheable):
3852
            seq.append(Event(_extract(node, "id"), self.network))
3853
3854
        return seq
3855
3856 1
    def get_recommended_artists(self, limit=50, cacheable=False):
3857
        """
3858
        Returns a sequence of Artist objects
3859
        if limit==None it will return all
3860
        """
3861
3862
        seq = []
3863
        for node in _collect_nodes(
3864
                limit, self, "user.getRecommendedArtists", cacheable):
3865
            seq.append(Artist(_extract(node, "name"), self.network))
3866
3867
        return seq
3868
3869
3870 1
class _Search(_BaseObject):
3871
    """An abstract class. Use one of its derivatives."""
3872
3873 1
    def __init__(self, ws_prefix, search_terms, network):
3874
        _BaseObject.__init__(self, network, ws_prefix)
3875
3876
        self._ws_prefix = ws_prefix
3877
        self.search_terms = search_terms
3878
3879
        self._last_page_index = 0
3880
3881 1
    def _get_params(self):
3882
        params = {}
3883
3884
        for key in self.search_terms.keys():
3885
            params[key] = self.search_terms[key]
3886
3887
        return params
3888
3889 1
    def get_total_result_count(self):
3890
        """Returns the total count of all the results."""
3891
3892
        doc = self._request(self._ws_prefix + ".search", True)
3893
3894
        return _extract(doc, "opensearch:totalResults")
3895
3896 1
    def _retrieve_page(self, page_index):
3897
        """Returns the node of matches to be processed"""
3898
3899
        params = self._get_params()
3900
        params["page"] = str(page_index)
3901
        doc = self._request(self._ws_prefix + ".search", True, params)
3902
3903
        return doc.getElementsByTagName(self._ws_prefix + "matches")[0]
3904
3905 1
    def _retrieve_next_page(self):
3906
        self._last_page_index += 1
3907
        return self._retrieve_page(self._last_page_index)
3908
3909
3910 1
class AlbumSearch(_Search):
3911
    """Search for an album by name."""
3912
3913 1
    def __init__(self, album_name, network):
3914
3915
        _Search.__init__(self, "album", {"album": album_name}, network)
3916
3917 1
    def get_next_page(self):
3918
        """Returns the next page of results as a sequence of Album objects."""
3919
3920
        master_node = self._retrieve_next_page()
3921
3922
        seq = []
3923
        for node in master_node.getElementsByTagName("album"):
3924
            seq.append(Album(
3925
                _extract(node, "artist"),
3926
                _extract(node, "name"),
3927
                self.network))
3928
3929
        return seq
3930
3931
3932 1
class ArtistSearch(_Search):
3933
    """Search for an artist by artist name."""
3934
3935 1
    def __init__(self, artist_name, network):
3936
        _Search.__init__(self, "artist", {"artist": artist_name}, network)
3937
3938 1
    def get_next_page(self):
3939
        """Returns the next page of results as a sequence of Artist objects."""
3940
3941
        master_node = self._retrieve_next_page()
3942
3943
        seq = []
3944
        for node in master_node.getElementsByTagName("artist"):
3945
            artist = Artist(_extract(node, "name"), self.network)
3946
            artist.listener_count = _number(_extract(node, "listeners"))
3947
            seq.append(artist)
3948
3949
        return seq
3950
3951
3952 1
class TagSearch(_Search):
3953
    """Search for a tag by tag name."""
3954
3955 1
    def __init__(self, tag_name, network):
3956
3957
        _Search.__init__(self, "tag", {"tag": tag_name}, network)
3958
3959 1
    def get_next_page(self):
3960
        """Returns the next page of results as a sequence of Tag objects."""
3961
3962
        master_node = self._retrieve_next_page()
3963
3964
        seq = []
3965
        for node in master_node.getElementsByTagName("tag"):
3966
            tag = Tag(_extract(node, "name"), self.network)
3967
            tag.tag_count = _number(_extract(node, "count"))
3968
            seq.append(tag)
3969
3970
        return seq
3971
3972
3973 1
class TrackSearch(_Search):
3974
    """
3975
    Search for a track by track title. If you don't want to narrow the results
3976
    down by specifying the artist name, set it to empty string.
3977
    """
3978
3979 1
    def __init__(self, artist_name, track_title, network):
3980
3981
        _Search.__init__(
3982
            self,
3983
            "track",
3984
            {"track": track_title, "artist": artist_name},
3985
            network)
3986
3987 1
    def get_next_page(self):
3988
        """Returns the next page of results as a sequence of Track objects."""
3989
3990
        master_node = self._retrieve_next_page()
3991
3992
        seq = []
3993
        for node in master_node.getElementsByTagName("track"):
3994
            track = Track(
3995
                _extract(node, "artist"),
3996
                _extract(node, "name"),
3997
                self.network)
3998
            track.listener_count = _number(_extract(node, "listeners"))
3999
            seq.append(track)
4000
4001
        return seq
4002
4003
4004 1
class VenueSearch(_Search):
4005
    """
4006
    Search for a venue by its name. If you don't want to narrow the results
4007
    down by specifying a country, set it to empty string.
4008
    """
4009
4010 1
    def __init__(self, venue_name, country_name, network):
4011
4012
        _Search.__init__(
4013
            self,
4014
            "venue",
4015
            {"venue": venue_name, "country": country_name},
4016
            network)
4017
4018 1
    def get_next_page(self):
4019
        """Returns the next page of results as a sequence of Track objects."""
4020
4021
        master_node = self._retrieve_next_page()
4022
4023
        seq = []
4024
        for node in master_node.getElementsByTagName("venue"):
4025
            seq.append(Venue(_extract(node, "id"), self.network))
4026
4027
        return seq
4028
4029
4030 1
class Venue(_BaseObject):
4031
    """A venue where events are held."""
4032
4033
    # TODO: waiting for a venue.getInfo web service to use.
4034
    # TODO: As an intermediate use case, can pass the venue DOM element when
4035
    # using Event.get_venue() to populate the venue info, if the venue.getInfo
4036
    # API call becomes available this workaround should be removed
4037
4038 1
    id = None
4039 1
    info = None
4040 1
    name = None
4041 1
    location = None
4042 1
    url = None
4043
4044 1
    __hash__ = _BaseObject.__hash__
4045
4046 1
    def __init__(self, netword_id, network, venue_element=None):
4047
        _BaseObject.__init__(self, network, "venue")
4048
4049
        self.id = _number(netword_id)
4050
        if venue_element is not None:
4051
            self.info = _extract_element_tree(venue_element)
4052
            self.name = self.info.get('name')
4053
            self.url = self.info.get('url')
4054
            self.location = self.info.get('location')
4055
4056 1
    def __repr__(self):
4057
        return "pylast.Venue(%s, %s)" % (repr(self.id), repr(self.network))
4058
4059 1
    @_string_output
4060
    def __str__(self):
4061
        return "Venue #" + str(self.id)
4062
4063 1
    def __eq__(self, other):
4064
        return self.get_id() == other.get_id()
4065
4066 1
    def _get_params(self):
4067
        return {self.ws_prefix: self.get_id()}
4068
4069 1
    def get_id(self):
4070
        """Returns the id of the venue."""
4071
4072
        return self.id
4073
4074 1
    def get_name(self):
4075
        """Returns the name of the venue."""
4076
4077
        return self.name
4078
4079 1
    def get_url(self):
4080
        """Returns the URL of the venue page."""
4081
4082
        return self.url
4083
4084 1
    def get_location(self):
4085
        """Returns the location of the venue (dictionary)."""
4086
4087
        return self.location
4088
4089 1
    def get_upcoming_events(self):
4090
        """Returns the upcoming events in this venue."""
4091
4092
        doc = self._request(self.ws_prefix + ".getEvents", True)
4093
4094
        return _extract_events_from_doc(doc, self.network)
4095
4096 1
    def get_past_events(self):
4097
        """Returns the past events held in this venue."""
4098
4099
        doc = self._request(self.ws_prefix + ".getEvents", True)
4100
4101
        return _extract_events_from_doc(doc, self.network)
4102
4103
4104 1
def md5(text):
4105
    """Returns the md5 hash of a string."""
4106
4107
    h = hashlib.md5()
4108
    h.update(_unicode(text).encode("utf-8"))
4109
4110
    return h.hexdigest()
4111
4112
4113 1
def _unicode(text):
4114 1
    if isinstance(text, six.binary_type):
4115 1
        return six.text_type(text, "utf-8")
4116 1
    elif isinstance(text, six.text_type):
4117 1
        return text
4118
    else:
4119 1
        return six.text_type(text)
4120
4121
4122 1
def _string(string):
4123
    """For Python2 routines that can only process str type."""
4124
    if isinstance(string, str):
4125
        return string
4126
    casted = six.text_type(string)
4127
    if sys.version_info[0] == 2:
4128
        casted = casted.encode("utf-8")
4129
    return casted
4130
4131
4132 1
def cleanup_nodes(doc):
4133
    """
4134
    Remove text nodes containing only whitespace
4135
    """
4136
    for node in doc.documentElement.childNodes:
4137
        if node.nodeType == Node.TEXT_NODE and node.nodeValue.isspace():
4138
            doc.documentElement.removeChild(node)
4139
    return doc
4140
4141
4142 1
def _collect_nodes(limit, sender, method_name, cacheable, params=None):
4143
    """
4144
    Returns a sequence of dom.Node objects about as close to limit as possible
4145
    """
4146
4147
    if not params:
4148
        params = sender._get_params()
4149
4150
    nodes = []
4151
    page = 1
4152
    end_of_pages = False
4153
4154
    while not end_of_pages and (not limit or (limit and len(nodes) < limit)):
4155
        params["page"] = str(page)
4156
        doc = sender._request(method_name, cacheable, params)
4157
        doc = cleanup_nodes(doc)
4158
4159
        main = doc.documentElement.childNodes[0]
4160
4161
        if main.hasAttribute("totalPages"):
4162
            total_pages = _number(main.getAttribute("totalPages"))
4163
        elif main.hasAttribute("totalpages"):
4164
            total_pages = _number(main.getAttribute("totalpages"))
4165
        else:
4166
            raise Exception("No total pages attribute")
4167
4168
        for node in main.childNodes:
4169
            if not node.nodeType == xml.dom.Node.TEXT_NODE and (
4170
                    not limit or (len(nodes) < limit)):
4171
                nodes.append(node)
4172
4173
        if page >= total_pages:
4174
            end_of_pages = True
4175
4176
        page += 1
4177
4178
    return nodes
4179
4180
4181 1
def _extract(node, name, index=0):
4182
    """Extracts a value from the xml string"""
4183
4184
    nodes = node.getElementsByTagName(name)
4185
4186
    if len(nodes):
4187
        if nodes[index].firstChild:
4188
            return _unescape_htmlentity(nodes[index].firstChild.data.strip())
4189
    else:
4190
        return None
4191
4192
4193 1
def _extract_element_tree(node):
4194
    """Extract an element tree into a multi-level dictionary
4195
4196
    NB: If any elements have text nodes as well as nested
4197
    elements this will ignore the text nodes"""
4198
4199
    def _recurse_build_tree(rootNode, targetDict):
4200
        """Recursively build a multi-level dict"""
4201
4202
        def _has_child_elements(rootNode):
4203
            """Check if an element has any nested (child) elements"""
4204
4205
            for node in rootNode.childNodes:
4206
                if node.nodeType == node.ELEMENT_NODE:
4207
                    return True
4208
            return False
4209
4210
        for node in rootNode.childNodes:
4211
            if node.nodeType == node.ELEMENT_NODE:
4212
                if _has_child_elements(node):
4213
                    targetDict[node.tagName] = {}
4214
                    _recurse_build_tree(node, targetDict[node.tagName])
4215
                else:
4216
                    val = None if node.firstChild is None else \
4217
                        _unescape_htmlentity(node.firstChild.data.strip())
4218
                    targetDict[node.tagName] = val
4219
        return targetDict
4220
4221
    return _recurse_build_tree(node, {})
4222
4223
4224 1
def _extract_all(node, name, limit_count=None):
4225
    """Extracts all the values from the xml string. returning a list."""
4226
4227
    seq = []
4228
4229
    for i in range(0, len(node.getElementsByTagName(name))):
4230
        if len(seq) == limit_count:
4231
            break
4232
4233
        seq.append(_extract(node, name, i))
4234
4235
    return seq
4236
4237
4238 1
def _extract_top_artists(doc, network):
4239
    # TODO Maybe include the _request here too?
4240
    seq = []
4241
    for node in doc.getElementsByTagName("artist"):
4242
        name = _extract(node, "name")
4243
        playcount = _extract(node, "playcount")
4244
4245
        seq.append(TopItem(Artist(name, network), playcount))
4246
4247
    return seq
4248
4249
4250 1
def _extract_top_albums(doc, network):
4251
    # TODO Maybe include the _request here too?
4252
    seq = []
4253
    for node in doc.getElementsByTagName("album"):
4254
        name = _extract(node, "name")
4255
        artist = _extract(node, "name", 1)
4256
        playcount = _extract(node, "playcount")
4257
4258
        seq.append(TopItem(Album(artist, name, network), playcount))
4259
4260
    return seq
4261
4262
4263 1
def _extract_artists(doc, network):
4264
    seq = []
4265
    for node in doc.getElementsByTagName("artist"):
4266
        seq.append(Artist(_extract(node, "name"), network))
4267
    return seq
4268
4269
4270 1
def _extract_albums(doc, network):
4271
    seq = []
4272
    for node in doc.getElementsByTagName("album"):
4273
        name = _extract(node, "name")
4274
        artist = _extract(node, "name", 1)
4275
        seq.append(Album(artist, name, network))
4276
    return seq
4277
4278
4279 1
def _extract_tracks(doc, network):
4280
    seq = []
4281
    for node in doc.getElementsByTagName("track"):
4282
        name = _extract(node, "name")
4283
        artist = _extract(node, "name", 1)
4284
        seq.append(Track(artist, name, network))
4285
    return seq
4286
4287
4288 1
def _extract_events_from_doc(doc, network):
4289
    events = []
4290
    for node in doc.getElementsByTagName("event"):
4291
        events.append(Event(_extract(node, "id"), network))
4292
    return events
4293
4294
4295 1
def _url_safe(text):
4296
    """Does all kinds of tricks on a text to make it safe to use in a url."""
4297
4298
    return url_quote_plus(url_quote_plus(_string(text))).lower()
4299
4300
4301 1
def _number(string):
4302
    """
4303
        Extracts an int from a string.
4304
        Returns a 0 if None or an empty string was passed.
4305
    """
4306
4307
    if not string:
4308
        return 0
4309
    elif string == "":
4310
        return 0
4311
    else:
4312
        try:
4313
            return int(string)
4314
        except ValueError:
4315
            return float(string)
4316
4317
4318 1
def _unescape_htmlentity(string):
4319
4320
    # string = _unicode(string)
4321
4322
    mapping = htmlentitydefs.name2codepoint
4323
    for key in mapping:
4324
        string = string.replace("&%s;" % key, unichr(mapping[key]))
4325
4326
    return string
4327
4328
4329 1
def extract_items(topitems_or_libraryitems):
4330
    """
4331
    Extracts a sequence of items from a sequence of TopItem or
4332
    LibraryItem objects.
4333
    """
4334
4335
    seq = []
4336
    for i in topitems_or_libraryitems:
4337
        seq.append(i.item)
4338
4339
    return seq
4340
4341
4342 1
class ScrobblingError(Exception):
4343 1
    def __init__(self, message):
4344
        Exception.__init__(self)
4345
        self.message = message
4346
4347 1
    @_string_output
4348
    def __str__(self):
4349
        return self.message
4350
4351
4352 1
class BannedClientError(ScrobblingError):
4353 1
    def __init__(self):
4354
        ScrobblingError.__init__(
4355
            self, "This version of the client has been banned")
4356
4357
4358 1
class BadAuthenticationError(ScrobblingError):
4359 1
    def __init__(self):
4360
        ScrobblingError.__init__(self, "Bad authentication token")
4361
4362
4363 1
class BadTimeError(ScrobblingError):
4364 1
    def __init__(self):
4365
        ScrobblingError.__init__(
4366
            self, "Time provided is not close enough to current time")
4367
4368
4369 1
class BadSessionError(ScrobblingError):
4370 1
    def __init__(self):
4371
        ScrobblingError.__init__(
4372
            self, "Bad session id, consider re-handshaking")
4373
4374
4375 1
class _ScrobblerRequest(object):
4376
4377 1
    def __init__(self, url, params, network, request_type="POST"):
4378
4379
        for key in params:
4380
            params[key] = str(params[key])
4381
4382
        self.params = params
4383
        self.type = request_type
4384
        (self.hostname, self.subdir) = url_split_host(url[len("http:"):])
4385
        self.network = network
4386
4387 1
    def execute(self):
4388
        """Returns a string response of this request."""
4389
4390
        if _can_use_ssl_securely():
4391
            connection = HTTPSConnection(
4392
                context=SSL_CONTEXT,
4393
                host=self.hostname
4394
            )
4395
        else:
4396
            connection = HTTPConnection(
4397
                host=self.hostname
4398
            )
4399
4400
        data = []
4401
        for name in self.params.keys():
4402
            value = url_quote_plus(self.params[name])
4403
            data.append('='.join((name, value)))
4404
        data = "&".join(data)
4405
4406
        headers = {
4407
            "Content-type": "application/x-www-form-urlencoded",
4408
            "Accept-Charset": "utf-8",
4409
            "User-Agent": "pylast" + "/" + __version__,
4410
            "HOST": self.hostname
4411
        }
4412
4413
        if self.type == "GET":
4414
            connection.request(
4415
                "GET", self.subdir + "?" + data, headers=headers)
4416
        else:
4417
            connection.request("POST", self.subdir, data, headers)
4418
        response = _unicode(connection.getresponse().read())
4419
4420
        self._check_response_for_errors(response)
4421
4422
        return response
4423
4424 1
    def _check_response_for_errors(self, response):
4425
        """
4426
        When passed a string response it checks for errors, raising any
4427
        exceptions as necessary.
4428
        """
4429
4430
        lines = response.split("\n")
4431
        status_line = lines[0]
4432
4433
        if status_line == "OK":
4434
            return
4435
        elif status_line == "BANNED":
4436
            raise BannedClientError()
4437
        elif status_line == "BADAUTH":
4438
            raise BadAuthenticationError()
4439
        elif status_line == "BADTIME":
4440
            raise BadTimeError()
4441
        elif status_line == "BADSESSION":
4442
            raise BadSessionError()
4443
        elif status_line.startswith("FAILED "):
4444
            reason = status_line[status_line.find("FAILED ") + len("FAILED "):]
4445
            raise ScrobblingError(reason)
4446
4447
4448 1
class Scrobbler(object):
4449
    """A class for scrobbling tracks to Last.fm"""
4450
4451 1
    session_id = None
4452 1
    nowplaying_url = None
4453 1
    submissions_url = None
4454
4455 1
    def __init__(self, network, client_id, client_version):
4456
        self.client_id = client_id
4457
        self.client_version = client_version
4458
        self.username = network.username
4459
        self.password = network.password_hash
4460
        self.network = network
4461
4462 1
    def _do_handshake(self):
4463
        """Handshakes with the server"""
4464
4465
        timestamp = str(int(time.time()))
4466
4467
        if self.password and self.username:
4468
            token = md5(self.password + timestamp)
4469
        elif self.network.api_key and self.network.api_secret and \
4470
                self.network.session_key:
4471
            if not self.username:
4472
                self.username = self.network.get_authenticated_user()\
4473
                    .get_name()
4474
            token = md5(self.network.api_secret + timestamp)
4475
4476
        params = {
4477
            "hs": "true", "p": "1.2.1", "c": self.client_id,
4478
            "v": self.client_version, "u": self.username, "t": timestamp,
4479
            "a": token}
4480
4481
        if self.network.session_key and self.network.api_key:
4482
            params["sk"] = self.network.session_key
4483
            params["api_key"] = self.network.api_key
4484
4485
        server = self.network.submission_server
4486
        response = _ScrobblerRequest(
4487
            server, params, self.network, "GET").execute().split("\n")
4488
4489
        self.session_id = response[1]
4490
        self.nowplaying_url = response[2]
4491
        self.submissions_url = response[3]
4492
4493 1
    def _get_session_id(self, new=False):
4494
        """
4495
        Returns a handshake. If new is true, then it will be requested from
4496
        the server even if one was cached.
4497
        """
4498
4499
        if not self.session_id or new:
4500
            self._do_handshake()
4501
4502
        return self.session_id
4503
4504 1
    def report_now_playing(
4505
            self, artist, title, album="", duration="", track_number="",
4506
            mbid=""):
4507
4508
        _deprecation_warning(
4509
            "DeprecationWarning: Use Network.update_now_playing(...) instead")
4510
4511
        params = {
4512
            "s": self._get_session_id(), "a": artist, "t": title,
4513
            "b": album, "l": duration, "n": track_number, "m": mbid}
4514
4515
        try:
4516
            _ScrobblerRequest(
4517
                self.nowplaying_url, params, self.network
4518
            ).execute()
4519
        except BadSessionError:
4520
            self._do_handshake()
4521
            self.report_now_playing(
4522
                artist, title, album, duration, track_number, mbid)
4523
4524 1
    def scrobble(
4525
            self, artist, title, time_started, source, mode, duration,
4526
            album="", track_number="", mbid=""):
4527
        """Scrobble a track. parameters:
4528
            artist: Artist name.
4529
            title: Track title.
4530
            time_started: UTC timestamp of when the track started playing.
4531
            source: The source of the track
4532
                SCROBBLE_SOURCE_USER: Chosen by the user
4533
                    (the most common value, unless you have a reason for
4534
                    choosing otherwise, use this).
4535
                SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised
4536
                    broadcast (e.g. Shoutcast, BBC Radio 1).
4537
                SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised
4538
                    recommendation except Last.fm (e.g. Pandora, Launchcast).
4539
                SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the
4540
                    5-digit recommendation_key value must be set.
4541
                SCROBBLE_SOURCE_UNKNOWN: Source unknown.
4542
            mode: The submission mode
4543
                SCROBBLE_MODE_PLAYED: The track was played.
4544
                SCROBBLE_MODE_LOVED: The user manually loved the track
4545
                    (implies a listen)
4546
                SCROBBLE_MODE_SKIPPED: The track was skipped
4547
                    (Only if source was Last.fm)
4548
                SCROBBLE_MODE_BANNED: The track was banned
4549
                    (Only if source was Last.fm)
4550
            duration: Track duration in seconds.
4551
            album: The album name.
4552
            track_number: The track number on the album.
4553
            mbid: MusicBrainz ID.
4554
        """
4555
4556
        _deprecation_warning(
4557
            "DeprecationWarning: Use Network.scrobble(...) instead")
4558
4559
        params = {
4560
            "s": self._get_session_id(),
4561
            "a[0]": _string(artist),
4562
            "t[0]": _string(title),
4563
            "i[0]": str(time_started),
4564
            "o[0]": source,
4565
            "r[0]": mode,
4566
            "l[0]": str(duration),
4567
            "b[0]": _string(album),
4568
            "n[0]": track_number,
4569
            "m[0]": mbid
4570
        }
4571
4572
        _ScrobblerRequest(self.submissions_url, params, self.network).execute()
4573
4574 1
    def scrobble_many(self, tracks):
4575
        """
4576
            Scrobble several tracks at once.
4577
4578
            tracks: A sequence of a sequence of parameters for each track.
4579
                The order of parameters is the same as if passed to the
4580
                scrobble() method.
4581
        """
4582
4583
        _deprecation_warning(
4584
            "DeprecationWarning: Use Network.scrobble_many(...) instead")
4585
4586
        remainder = []
4587
4588
        if len(tracks) > 50:
4589
            remainder = tracks[50:]
4590
            tracks = tracks[:50]
4591
4592
        params = {"s": self._get_session_id()}
4593
4594
        i = 0
4595
        for t in tracks:
4596
            _pad_list(t, 9, "")
4597
            params["a[%s]" % str(i)] = _string(t[0])
4598
            params["t[%s]" % str(i)] = _string(t[1])
4599
            params["i[%s]" % str(i)] = str(t[2])
4600
            params["o[%s]" % str(i)] = t[3]
4601
            params["r[%s]" % str(i)] = t[4]
4602
            params["l[%s]" % str(i)] = str(t[5])
4603
            params["b[%s]" % str(i)] = _string(t[6])
4604
            params["n[%s]" % str(i)] = t[7]
4605
            params["m[%s]" % str(i)] = t[8]
4606
4607
            i += 1
4608
4609
        _ScrobblerRequest(self.submissions_url, params, self.network).execute()
4610
4611
        if remainder:
4612
            self.scrobble_many(remainder)
4613
4614
# End of file
4615