a/src/mediaserver/cdplugins/tidal/tidalapi/__init__.py b/src/mediaserver/cdplugins/tidal/tidalapi/__init__.py
...
...
13
# GNU Lesser General Public License for more details.
13
# GNU Lesser General Public License for more details.
14
#
14
#
15
# You should have received a copy of the GNU Lesser General Public License
15
# You should have received a copy of the GNU Lesser General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
17
18
from __future__ import unicode_literals
18
from __future__ import unicode_literals, print_function
19
19
20
import sys
21
import re
20
import datetime
22
import datetime
21
import json
23
import json
24
import random
22
import logging
25
import logging
23
import requests
26
import requests
27
from requests.packages import urllib3
24
from collections import namedtuple
28
from collections import namedtuple
29
from .models import SubscriptionType, Quality
25
from .models import Artist, Album, Track, Playlist, SearchResult, Category
30
from .models import Artist, Album, Track, Playlist, SearchResult, Category
26
try:
31
try:
27
    from urlparse import urljoin
32
    from urlparse import urljoin
28
except ImportError:
33
except ImportError:
29
    from urllib.parse import urljoin
34
    from urllib.parse import urljoin
30
35
36
class MLog(object):
37
    def __init__(self):
38
        self.f = sys.stderr
39
        self.level = 1
40
    def isEnabledFor(self, l):
41
        return True
42
    def debug(self, msg):
43
        if self.level >= 3:
44
            print("%s" % msg, file=self.f)
45
    def info(self, msg):
46
        if self.level >= 2:
47
            print("%s" % msg, file=self.f)
48
    def error(self, msg):
49
        if self.level >= 1:
50
            print("%s" % msg, file=self.f)
31
51
32
log = logging.getLogger(__name__)
52
#log = logging.getLogger(__name__)
53
log = MLog()
33
54
34
Api = namedtuple('API', ['location', 'token'])
55
# See https://github.com/arnesongit/python-tidal/ for token descs
35
36
37
class Quality(object):
38
    lossless = 'LOSSLESS'
39
    high = 'HIGH'
40
    low = 'LOW'
41
42
43
class Config(object):
56
class Config(object):
44
    def __init__(self, quality=Quality.high):
57
    def __init__(self, quality=Quality.high):
45
        self.quality = quality
58
        self.quality = quality
46
        self.api_location = 'https://api.tidalhifi.com/v1/'
59
        self.api_location = 'https://api.tidal.com/v1/'
47
        # For some reason, Kodi uses the following keys. The 2nd one results
60
        self.api_token = 'kgsOOmYk3zShYrNP'
48
        # in rtmp / flv stream containers which are ennoying to handle. No idea
61
        self.preview_token = "8C7kRFdkaRp0dLBp" # Token for Preview Mode
49
        # where it comes from (the other is from the older wimpy api), or why
50
        # they do this.
51
        self.api_token = 'P5Xbeo5LFvESeDy6' if self.quality == \
52
                         Quality.lossless else 'wdgaB1CilGA-S_s2'
53
62
54
63
55
class Session(object):
64
class Session(object):
56
65
57
    def __init__(self, config=Config()):
66
    def __init__(self, config=Config()):
67
        """:type _config: :class:`Config`"""
68
        self._config = config
58
        self.session_id = None
69
        self.session_id = None
59
        self.country_code = None
60
        self.user = None
70
        self.user = None
61
        self._config = config
71
        self.country_code = 'US'   # Enable Trial Mode
62
        """:type _config: :class:`Config`"""
72
        self.client_unique_key = None
73
        urllib3.disable_warnings() # Disable OpenSSL Warnings in URLLIB3
63
74
75
    def logout(self):
76
        self.session_id = None
77
        self.user = None
78
64
    def load_session(self, session_id, country_code, user_id):
79
    def load_session(self, session_id, country_code, user_id=None,
80
                     subscription_type=None, unique_key=None):
65
        self.session_id = session_id
81
        self.session_id = session_id
82
        self.client_unique_key = unique_key
66
        self.country_code = country_code
83
        self.country_code = country_code
67
        self.user = User(self, id=user_id)
84
        if not self.country_code:
85
            # Set Local Country Code to enable Trial Mode 
86
            self.country_code = self.local_country_code()
87
        if user_id:
88
            self.user = self.init_user(user_id=user_id,
89
                                       subscription_type=subscription_type)
90
        else:
91
            self.user = None
68
92
93
    def generate_client_unique_key(self):
94
        return format(random.getrandbits(64), '02x')
95
69
    def login(self, username, password):
96
    def login(self, username, password, subscription_type=None):
97
        self.logout()
98
        if not username or not password:
99
            return False
100
        if not subscription_type:
101
            # Set Subscription Type corresponding to the given playback quality
102
            subscription_type = SubscriptionType.hifi if \
103
                                self._config.quality == Quality.lossless else \
104
                                SubscriptionType.premium
105
        if not self.client_unique_key:
106
            # Generate a random client key if no key is given
107
            self.client_unique_key = self.generate_client_unique_key()
70
        url = urljoin(self._config.api_location, 'login/username')
108
        url = urljoin(self._config.api_location, 'login/username')
71
        params = {'token': self._config.api_token}
109
        headers = { "X-Tidal-Token": self._config.api_token }
72
        payload = {
110
        payload = {
73
            'username': username,
111
            'username': username,
74
            'password': password,
112
            'password': password,
113
            'clientUniqueKey': self.client_unique_key
75
        }
114
        }
115
        log.debug('Using Token "%s" with clientUniqueKey "%s"' %
116
                  (self._config.api_token, self.client_unique_key))
76
        r = requests.post(url, data=payload, params=params)
117
        r = requests.post(url, data=payload, headers=headers)
77
        r.raise_for_status()
118
        if not r.ok:
119
            try:
120
                msg = r.json().get('userMessage')
121
            except:
122
                msg = r.reason
123
            log.error(msg)
124
        else:
125
            try:
78
        body = r.json()
126
                body = r.json()
79
        self.session_id = body['sessionId']
127
                self.session_id = body['sessionId']
80
        self.country_code = body['countryCode']
128
                self.country_code = body['countryCode']
81
        self.user = User(self, id=body['userId'])
129
                self.user = self.init_user(user_id=body['userId'],
82
        return True
130
                                           subscription_type=subscription_type)
131
            except Exception as err:
132
                log.error('Login failed. err %s %s' % (err, body))
133
                self.logout()
83
134
135
        return self.is_logged_in
136
137
    def init_user(self, user_id, subscription_type):
138
        return User(self, user_id=user_id, subscription_type=subscription_type)
139
140
    def local_country_code(self):
141
        url = urljoin(self._config.api_location, 'country/context')
142
        headers = { "X-Tidal-Token": self._config.api_token}
143
        r = requests.request('GET', url, params={'countryCode': 'WW'},
144
                             headers=headers)
145
        if not r.ok:
146
            return 'US'
147
        return r.json().get('countryCode')
148
149
    @property
150
    def is_logged_in(self):
151
        return True if self.session_id and self.country_code and self.user \
152
               else False
153
    
84
    def check_login(self):
154
    def check_login(self):
85
        """ Returns true if current session is valid, false otherwise. """
155
        """ Returns true if current session is valid, false otherwise. """
86
        if self.user is None or not self.user.id or not self.session_id:
156
        if not self.is_logged_in:
87
            return False
157
            return False
88
        url = urljoin(self._config.api_location, 'users/%s/subscription' % self.user.id)
158
        self.user.subscription = self.get_user_subscription(self.user.id)
89
        return requests.get(url, params={'sessionId': self.session_id}).ok
159
        return True if self.user.subscription != None else False
90
160
91
    def request(self, method, path, params=None, data=None):
161
    def request(self, method, path, params=None, data=None, headers=None):
162
        request_headers = {}
92
        request_params = {
163
        request_params = {
93
            'sessionId': self.session_id,
164
            'sessionId': self.session_id,
94
            'countryCode': self.country_code,
165
            'countryCode': self.country_code,
95
            'limit': '999',
166
            'limit': '999',
96
        }
167
        }
168
        if headers:
169
            request_headers.update(headers)
97
        if params:
170
        if params:
98
            request_params.update(params)
171
            request_params.update(params)
99
        url = urljoin(self._config.api_location, path)
172
        url = urljoin(self._config.api_location, path)
173
        if self.is_logged_in:
174
            # Request with API Session if SessionId is not given in headers parameter
175
            if not 'X-Tidal-SessionId' in request_headers:
176
                request_headers.update({'X-Tidal-SessionId': self.session_id})
177
        else:
178
            # Request with Preview-Token. Remove SessionId if given via headers parameter
179
            request_headers.pop('X-Tidal-SessionId', None)
180
            request_params.update({'token': self._config.preview_token})
100
        r = requests.request(method, url, params=request_params, data=data)
181
        r = requests.request(method, url, params=request_params, data=data, headers=request_headers)
101
        log.debug("request: %s" % r.request.url)
182
        log.debug("%s %s" % (method, r.request.url))
183
        if not r.ok:
184
            log.error(r.url)
185
            try:
186
                log.error(r.json().get('userMessage'))
187
            except:
188
                log.error(r.reason)
102
        r.raise_for_status()
189
        r.raise_for_status()
103
        if r.content:
190
        if r.content and log.isEnabledFor(logging.INFO):
104
            log.debug("response: %s" % json.dumps(r.json(), indent=4))
191
            log.info("response: %s" % json.dumps(r.json(), indent=4))
105
        return r
192
        return r
106
193
107
    def get_user(self, user_id):
194
    def get_user(self, user_id):
108
        return self._map_request('users/%s' % user_id, ret='user')
195
        return self._map_request('users/%s' % user_id, ret='user')
196
197
    def get_user_subscription(self, user_id):
198
        return self._map_request('users/%s/subscription' % user_id, ret='subscription')
109
199
110
    def get_user_playlists(self, user_id):
200
    def get_user_playlists(self, user_id):
111
        return self._map_request('users/%s/playlists' % user_id, ret='playlists')
201
        return self._map_request('users/%s/playlists' % user_id, ret='playlists')
112
202
113
    def get_playlist(self, playlist_id):
203
    def get_playlist(self, playlist_id):
...
...
337
427
338
class User(object):
428
class User(object):
339
429
340
    favorites = None
430
    favorites = None
341
431
342
    def __init__(self, session, id):
432
    def __init__(self, session, user_id, subscription_type=SubscriptionType.hifi):
343
        """
344
        :type session: :class:`Session`
345
        :param id: The user ID
346
        """
347
        self._session = session
433
        self._session = session
348
        self.id = id
434
        self.id = user_id
349
        self.favorites = Favorites(session, self.id)
435
        self.favorites = Favorites(session, self.id)
350
436
        
351
    def playlists(self):
437
    def playlists(self):
352
        return self._session.get_user_playlists(self.id)
438
        return self._session.get_user_playlists(self.id)