# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 Arne Svenson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals

import os, sys, re
import locale
import json
import datetime
from urlparse import urlsplit
import xbmc
import xbmcvfs
import xbmcgui
import xbmcaddon
import xbmcplugin
import requests
from xbmcgui import ListItem
from routing import Plugin
from tidalapi import Config, Session, User, Favorites
from tidalapi.models import Quality, SubscriptionType, AlbumType, BrowsableMedia, Artist, Album, PlayableMedia, Track, Video, Mix, Playlist, Promotion, Category, CutInfo
from m3u8 import load as m3u8_load
from debug import DebugHelper

class KodiPlugin(Plugin):
    def __init__(self, base_url=None):
        try:
            # Creates a Dump is sys.argv[] is empty !
            Plugin.__init__(self, base_url=base_url)
        except:
            pass
        self.base_url = base_url

_addon_id = 'plugin.audio.tidal2'
addon = xbmcaddon.Addon(id=_addon_id)
plugin = KodiPlugin(base_url = "plugin://" + _addon_id)
plugin.name = addon.getAddonInfo('name')
_addon_icon = os.path.join(addon.getAddonInfo('path').decode('utf-8'), 'icon.png')
_addon_fanart = os.path.join(addon.getAddonInfo('path').decode('utf-8'), 'fanart.jpg')

debug = DebugHelper(pluginName=addon.getAddonInfo('name'), 
                    detailLevel=2 if addon.getSetting('debug_log') == 'true' else 1, 
                    enableTidalApiLog= True if addon.getSetting('debug_log') == 'true' else False)

log = debug.log

try:
    version = json.loads(xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["version", "name"]}, "id": 1 }'))['result']['version']
    KODI_VERSION = (version['major'], version['minor'])
except:
    KODI_VERSION = (16, 1)

CACHE_DIR = xbmc.translatePath(addon.getAddonInfo('profile')).decode('utf-8')
FAVORITES_FILE = os.path.join(CACHE_DIR, 'favorites.cfg')
PLAYLISTS_FILE = os.path.join(CACHE_DIR, 'playlists.cfg')
ALBUM_PLAYLIST_TAG = 'ALBUM'
VARIOUS_ARTIST_ID = '2935'


def _T(txtid):
    if isinstance(txtid, basestring):
        # Map TIDAL texts to Text IDs
        newid = {'artist':  30101, 'album':  30102, 'playlist':  30103, 'track':  30104, 'video':  30105,
                 'artists': 30101, 'albums': 30102, 'playlists': 30103, 'tracks': 30104, 'videos': 30105,
                 'featured': 30203, 'rising': 30211, 'discovery': 30212, 'movies': 30115, 'shows': 30116, 'genres': 30117, 'moods': 30118
                 }.get(txtid.lower(), None)
        if not newid: return txtid
        txtid = newid
    try:
        txt = addon.getLocalizedString(txtid)
        return txt
    except:
        return '%s' % txtid


def _P(key, default_txt=None):
    # Plurals of some Texts
    newid = {'new': 30111, 'local': 30112, 'exclusive': 30113, 'recommended': 30114, 'top': 30119,
             'artists': 30106, 'albums': 30107, 'playlists': 30108, 'tracks': 30109, 'videos': 30110
             }.get(key.lower(), None)
    if newid:
        return _T(newid)
    return default_txt if default_txt else key


# Convert TIDAL-API Media into Kodi List Items

class HasListItem(object):

    _is_logged_in = False

    def setLabelFormat(self):
        self._favorites_in_labels = True if addon.getSetting('favorites_in_labels') == 'true' else False
        self._user_playlists_in_labels = True if addon.getSetting('user_playlists_in_labels') == 'true' else False
        self.FOLDER_MASK = '{label}'
        if self._favorites_in_labels:
            self.FAVORITE_MASK = '<{label}>'
        else:
            self.FAVORITE_MASK = '{label}'
        self.STREAM_LOCKED_MASK = '{label} ({info})'
        if self._user_playlists_in_labels:
            self.USER_PLAYLIST_MASK = '{label} [{userpl}]'
        else:
            self.USER_PLAYLIST_MASK = '{label}'
        self.DEFAULT_PLAYLIST_MASK = '{label} ({mediatype})'
        self.MASTER_AUDIO_MASK = '{label} (MQA)'

    def getLabel(self, extended=True):
        return self.name

    def getListItem(self):
        li = ListItem(self.getLabel())
        if isinstance(self, PlayableMedia) and getattr(self, 'available', True):
            li.setProperty('isplayable', 'true')
        artwork = {'thumb': _addon_icon, 'fanart': _addon_fanart}
        if getattr(self, 'image', None):
            artwork['thumb'] = self.image
        if getattr(self, 'fanart', None):
            artwork['fanart'] = self.fanart
        li.setArt(artwork)
        # In Favorites View everything as a Favorite
        if self._is_logged_in and hasattr(self, '_isFavorite') and '/favorites/' in sys.argv[0]:
            self._isFavorite = True
        cm = self.getContextMenuItems()
        if len(cm) > 0:
            li.addContextMenuItems(cm)
        return li

    def getContextMenuItems(self):
        return []

    def getSortText(self, mode=None):
        return self.getLabel(extended=False)


class AlbumItem(Album, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self.artist = ArtistItem(self.artist)
        self.artists = [ArtistItem(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem(artist) for artist in self._ftArtists]
        self._userplaylists = {}    # Filled by parser
        self._playlist_id = None    # ID of the Playlist
        self._playlist_pos = -1     # Item position in playlist
        self._etag = None           # ETag for User Playlists
        self._playlist_name = None  # Name of Playlist
        self._playlist_type = ''    # Playlist Type
        self._playlist_track_id = 0 # Track-ID of item which is shown as Album Item

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label = self.getLongTitle()
        if extended and self._isFavorite and not '/favorites/' in sys.argv[0]:
            label = self.FAVORITE_MASK.format(label=label)
        label = '%s - %s' % (self.artist.getLabel(extended), label)
        txt = []
        plids = self._userplaylists.keys()
        for plid in plids:
            if plid <> self._playlist_id:
                txt.append('%s' % self._userplaylists.get(plid).get('title'))
        if extended and txt:
            label = self.USER_PLAYLIST_MASK.format(label=label, userpl=', '.join(txt))
        return label

    def getLongTitle(self):
        self.setLabelFormat()
        longTitle = '%s' % self.title
        if self.type == AlbumType.ep:
            longTitle += ' (EP)'
        elif self.type == AlbumType.single:
            longTitle += ' (Single)'
        if self.explicit and not 'Explicit' in self.title:
            longTitle += ' (Explicit)'
        if getattr(self, 'year', None) and addon.getSetting('album_year_in_labels') == 'true':
            if self.releaseDate and self.releaseDate > datetime.datetime.now():
                longTitle += ' (%s)' % _T(30268).format(self.releaseDate)
            else:
                longTitle += ' (%s)' % self.year
        if self.audioQuality == Quality.hi_res and addon.getSetting('mqa_in_labels') == 'true':
            longTitle = self.MASTER_AUDIO_MASK.format(label=longTitle)
        return longTitle

    def getSortText(self, mode=None):
        return '%s - (%s) %s' % (self.artist.getLabel(extended=False), getattr(self, 'year', ''), self.getLongTitle())

    def getListItem(self):
        li = HasListItem.getListItem(self)
        url = plugin.url_for_path('/album/%s' % self.id)
        infoLabels = {
            'title': self.title,
            'album': self.title,
            'artist': self.artist.name,
            'year': getattr(self, 'year', None),
            'tracknumber': self._itemPosition + 1 if self._itemPosition >= 0 else 0,
        }
        try:
            if self.streamStartDate:
                infoLabels.update({'date': self.streamStartDate.date().strftime('%d.%m.%Y')})
            elif self.releaseDate:
                infoLabels.update({'date': self.releaseDate.date().strftime('%d.%m.%Y')})
        except:
            pass
        if KODI_VERSION >= (17, 0):
            infoLabels.update({'mediatype': 'album',
                               'rating': '%s' % int(round(self.popularity / 10.0)),
                               'userrating': '%s' % int(round(self.popularity / 10.0))
                               })
        li.setInfo('music', infoLabels)
        return (url, li, True)

    def getContextMenuItems(self):
        cm = []
        if self._is_logged_in:
            if self._isFavorite:
                cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/albums/%s' % self.id)))
            else:
                cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/albums/%s' % self.id)))
            if self._playlist_type == 'USER':
                cm.append((_T(30240), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove/%s/%s' % (self._playlist_id, self._playlist_pos))))
                cm.append((_T(30248), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/move/%s/%s/%s' % (self._playlist_id, self._playlist_pos, self._playlist_track_id))))
            cm.append((_T(30239), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/add/album/%s' % self.id)))
            plids = self._userplaylists.keys()
            for plid in plids:
                if plid <> self._playlist_id:
                    cm.append(((_T(30247).format(name=self._userplaylists[plid].get('title')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove_album/%s/%s' % (plid, self.id)))))
            cm.append((_T(30221), 'Container.Update(%s)' % plugin.url_for_path('/artist/%s' % self.artist.id)))
        return cm


class ArtistItem(Artist, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self._isLocked = True if VARIOUS_ARTIST_ID == '%s' % self.id else False

    def getLabel(self, extended=True):
        self.setLabelFormat()
        if extended and self._isFavorite and not '/favorites/artists' in sys.argv[0]:
            return self.FAVORITE_MASK.format(label=self.name)
        if self._isLocked and '/favorites/artists' in sys.argv[0]:
            return self.STREAM_LOCKED_MASK.format(label=self.name, info=_T(30260))
        return self.name

    def getListItem(self):
        li = HasListItem.getListItem(self)
        url = plugin.url_for_path('/artist/%s' % self.id)
        infoLabel = {'artist': self.name}
        if KODI_VERSION >= (17, 0):
            infoLabel.update({'mediatype': 'artist',
                              'rating': '%s' % int(round(self.popularity / 10.0)),
                              'userrating': '%s' % int(round(self.popularity / 10.0))
                              })
        li.setInfo('music', infoLabel)
        return (url, li, True)

    def getContextMenuItems(self):
        cm = []
        if self._is_logged_in:
            if self._isFavorite:
                cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/artists/%s' % self.id)))
            else:
                cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/artists/%s' % self.id)))
            if '/favorites/artists' in sys.argv[0]:
                if self._isLocked:
                    cm.append((_T(30262), 'RunPlugin(%s)' % plugin.url_for_path('/unlock_artist/%s' % self.id)))
                else:
                    cm.append((_T(30261), 'RunPlugin(%s)' % plugin.url_for_path('/lock_artist/%s' % self.id)))
        return cm


class MixItem(Mix, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label = self.name
        return label

    def getListItem(self):
        li = HasListItem.getListItem(self)
        url = plugin.url_for_path('/mix/%s' % self.id)
        infoLabel = {
            'title': self.title,
            'album': self.subTitle
        }
        li.setInfo('music', infoLabel)
        return (url, li, True)


class PlaylistItem(Playlist, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        # Fix negative number of tracks/videos in playlist
        if self.numberOfItems > 0 and self.numberOfTracks < 0:
            self.numberOfVideos += self.numberOfTracks
            self.numberOfTracks = 0
        if self.numberOfItems > 0 and self.numberOfVideos < 0:
            self.numberOfTracks += self.numberOfVideos
            self.numberOfVideos = 0
        if self.numberOfItems < 0:
            self.numberOfTracks = self.numberOfVideos = 0

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label = self.name
        if extended and self._isFavorite and not '/favorites/' in sys.argv[0]:
            label = self.FAVORITE_MASK.format(label=label)
        if self.type == 'USER' and sys.argv[0].lower().find('user_playlists') >= 0:
            defaultpl = []
            if str(self.id) == addon.getSetting('default_trackplaylist_id'):
                defaultpl.append(_P('tracks'))
            if str(self.id) == addon.getSetting('default_videoplaylist_id'):
                defaultpl.append(_P('videos'))
            if str(self.id) == addon.getSetting('default_albumplaylist_id'):
                defaultpl.append(_P('albums'))
            if len(defaultpl) > 0:
                return self.DEFAULT_PLAYLIST_MASK.format(label=label, mediatype=', '.join(defaultpl))
        return label

    def getListItem(self):
        li = HasListItem.getListItem(self)
        path = '/playlist/%s/items/0'
        if self.type == 'USER' and ALBUM_PLAYLIST_TAG in self.description:
            path = '/playlist/%s/albums/0'
        url = plugin.url_for_path(path % self.id)
        infoLabel = {
            'artist': self.title,
            'album': self.description,
            'title': _T(30243).format(tracks=self.numberOfTracks, videos=self.numberOfVideos),
            'genre': _T(30243).format(tracks=self.numberOfTracks, videos=self.numberOfVideos),
            'tracknumber': self._itemPosition + 1 if self._itemPosition >= 0 else 0
        }
        try:
            if self.lastUpdated:
                infoLabel.update({'date': self.lastUpdated.date().strftime('%d.%m.%Y')})
            elif self.creationDate:
                infoLabel.update({'date': self.creationDate.date().strftime('%d.%m.%Y')})
        except:
            pass
        if KODI_VERSION >= (17, 0):
            infoLabel.update({'userrating': '%s' % int(round(self.popularity / 10.0))})
        li.setInfo('music', infoLabel)
        return (url, li, True)

    def getContextMenuItems(self):
        cm = []
        if self.numberOfVideos > 0:
            cm.append((_T(30252), 'Container.Update(%s)' % plugin.url_for_path('/playlist/%s/tracks/0' % self.id)))
        if self.type == 'USER' and ALBUM_PLAYLIST_TAG in self.description:
            cm.append((_T(30254), 'Container.Update(%s)' % plugin.url_for_path('/playlist/%s/items/0' % self.id)))
        else:
            cm.append((_T(30255), 'Container.Update(%s)' % plugin.url_for_path('/playlist/%s/albums/0' % self.id)))
        if self._is_logged_in:
            if self.type == 'USER':
                cm.append((_T(30251), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/rename/%s' % self.id)))
                if self.numberOfItems > 0:
                    cm.append((_T(30258), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/clear/%s' % self.id)))
                cm.append((_T(30235), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/delete/%s' % self.id)))
            else:
                if self._isFavorite:
                    cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/playlists/%s' % self.id)))
                else:
                    cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/playlists/%s' % self.id)))
            cm.append((_T(30239), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/add/playlist/%s' % self.id)))
            if self.type == 'USER' and sys.argv[0].lower().find('user_playlists') >= 0:
                if str(self.id) == addon.getSetting('default_trackplaylist_id'):
                    cm.append((_T(30250).format(what=_T('Track')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_reset_default/tracks')))
                else:
                    cm.append((_T(30249).format(what=_T('Track')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_set_default/tracks/%s' % self.id)))
                if str(self.id) == addon.getSetting('default_videoplaylist_id'):
                    cm.append((_T(30250).format(what=_T('Video')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_reset_default/videos')))
                else:
                    cm.append((_T(30249).format(what=_T('Video')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_set_default/videos/%s' % self.id)))
                if str(self.id) == addon.getSetting('default_albumplaylist_id'):
                    cm.append((_T(30250).format(what=_T('Album')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_reset_default/albums')))
                else:
                    cm.append((_T(30249).format(what=_T('Album')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist_set_default/albums/%s' % self.id)))
        return cm


class TrackItem(Track, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        if self.version and not self.version in self.title:
            self.title += ' (%s)' % self.version
            self.version = None
        self.artist = ArtistItem(self.artist)
        self.artists = [ArtistItem(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem(artist) for artist in self._ftArtists]
        self.album = AlbumItem(self.album)
        self._userplaylists = {} # Filled by parser

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label1 = self.artist.getLabel(extended=extended if self.available else False)
        label2 = self.getLongTitle()
        if extended and self._isFavorite and self.available and not '/favorites/' in sys.argv[0]:
            label2 = self.FAVORITE_MASK.format(label=label2)
        label = '%s - %s' % (label1, label2)
        if extended and not self.available:
            label = self.STREAM_LOCKED_MASK.format(label=label, info=_T(30242))
        txt = []
        plids = self._userplaylists.keys()
        for plid in plids:
            if plid <> self._playlist_id:
                txt.append('%s' % self._userplaylists.get(plid).get('title'))
        if extended and txt:
            label = self.USER_PLAYLIST_MASK.format(label=label, userpl=', '.join(txt))
        return label

    def getLongTitle(self):
        self.setLabelFormat()
        longTitle = self.title
        if self.version and not self.version in self.title:
            longTitle += ' (%s)' % self.version
        if self.explicit and not 'Explicit' in self.title:
            longTitle += ' (Explicit)'
        if self.editable and isinstance(self._cut, CutInfo):
            if self._cut.name:
                longTitle += ' (%s)' % self._cut.name
        if self.audioQuality == Quality.hi_res and addon.getSetting('mqa_in_labels') == 'true':
            longTitle = self.MASTER_AUDIO_MASK.format(label=longTitle)
        return longTitle

    def getSortText(self, mode=None):
        if mode == 'ALBUM':
            return self.album.getSortText(mode=mode)
        return self.getLabel(extended=False)

    def getFtArtistsText(self):
        text = ''
        for item in self._ftArtists:
            if len(text) > 0:
                text = text + ', '
            text = text + item.name
        if len(text) > 0:
            text = 'ft. by ' + text
        return text

    def getComment(self):
        return self.getFtArtistsText()

    def getListItem(self):
        li = HasListItem.getListItem(self)
        if self.available:
            if isinstance(self._cut, CutInfo):
                url = plugin.url_for_path('/play_track_cut/%s/%s/%s' % (self.id, self._cut.id, self.album.id))
            else:
                url = plugin.url_for_path('/play_track/%s/%s' % (self.id, self.album.id))
            isFolder = False
        else:
            url = plugin.url_for_path('/stream_locked')
            isFolder = True
        longTitle = self.title
        if self.explicit and not 'Explicit' in self.title:
            longTitle += ' (Explicit)'
        infoLabel = {
            'title': longTitle,
            'tracknumber': self._playlist_pos + 1 if self._playlist_id else self._itemPosition + 1 if self._itemPosition >= 0 else self.trackNumber,
            'discnumber': self.volumeNumber,
            'duration': self.duration,
            'artist': self.artist.name,
            'album': self.album.title,
            'year': getattr(self, 'year', None),
            'rating': '%s' % int(round(self.popularity / 10.0)),
            'comment': self.getComment()
        }
        try:
            if self.streamStartDate:
                infoLabel.update({'date': self.streamStartDate.date().strftime('%d.%m.%Y')})
            elif self.releaseDate:
                infoLabel.update({'date': self.releaseDate.date().strftime('%d.%m.%Y')})
        except:
            pass
        if KODI_VERSION >= (17, 0):
            infoLabel.update({'mediatype': 'song',
                              'userrating': '%s' % int(round(self.popularity / 10.0))
                              })
        li.setInfo('music', infoLabel)
        return (url, li, isFolder)

    def getContextMenuItems(self):
        cm = []
        if self._is_logged_in:
            if self._isFavorite:
                cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/tracks/%s' % self.id)))
            else:
                cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/tracks/%s' % self.id)))
            if self._playlist_type == 'USER':
                cm.append((_T(30240), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove/%s/%s' % (self._playlist_id, self._playlist_pos))))
                item_id = self.id if not isinstance(self._cut, CutInfo) else self._cut.id
                cm.append((_T(30248), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/move/%s/%s/%s' % (self._playlist_id, self._playlist_pos, item_id))))
            else:
                cm.append((_T(30239), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/add/track/%s' % self.id)))
            plids = self._userplaylists.keys()
            for plid in plids:
                if plid <> self._playlist_id:
                    playlist = self._userplaylists[plid]
                    if '%s' % self.album.id in playlist.get('album_ids', []):
                        cm.append(((_T(30247).format(name=playlist.get('title')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove_album/%s/%s' % (plid, self.album.id)))))
                    else:
                        cm.append(((_T(30247).format(name=playlist.get('title')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove_id/%s/%s' % (plid, self.id)))))
        cm.append((_T(30221), 'Container.Update(%s)' % plugin.url_for_path('/artist/%s' % self.artist.id)))
        cm.append((_T(30245), 'Container.Update(%s)' % plugin.url_for_path('/album/%s' % self.album.id)))
        cm.append((_T(30222), 'Container.Update(%s)' % plugin.url_for_path('/track_radio/%s' % self.id)))
        cm.append((_T(30223), 'Container.Update(%s)' % plugin.url_for_path('/recommended/tracks/%s' % self.id)))
        return cm


class VideoItem(Video, HasListItem):

    def __init__(self, item):
        self.__dict__.update(vars(item))
        self.artist = ArtistItem(self.artist)
        self.artists = [ArtistItem(artist) for artist in self.artists]
        self._ftArtists = [ArtistItem(artist) for artist in self._ftArtists]
        self._userplaylists = {} # Filled by parser

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label1 = self.artist.name
        if extended and self.artist._isFavorite and self.available:
            label1 = self.FAVORITE_MASK.format(label=label1)
        label2 = self.getLongTitle()
        if extended and self._isFavorite and self.available and not '/favorites/' in sys.argv[0]:
            label2 = self.FAVORITE_MASK.format(label=label2)
        label = '%s - %s' % (label1, label2)
        if extended and not self.available:
            label = self.STREAM_LOCKED_MASK.format(label=label, info=_T(30242))
        txt = []
        plids = self._userplaylists.keys()
        for plid in plids:
            if plid <> self._playlist_id:
                txt.append('%s' % self._userplaylists.get(plid).get('title'))
        if extended and txt:
            label = self.USER_PLAYLIST_MASK.format(label=label, userpl=', '.join(txt))
        return label

    def getLongTitle(self):
        longTitle = self.title
        if self.explicit and not 'Explicit' in self.title:
            longTitle += ' (Explicit)'
        if getattr(self, 'year', None):
            longTitle += ' (%s)' % self.year
        return longTitle

    def getFtArtistsText(self):
        text = ''
        for item in self._ftArtists:
            if len(text) > 0:
                text = text + ', '
            text = text + item.name
        if len(text) > 0:
            text = 'ft. by ' + text
        return text

    def getComment(self):
        return self.getFtArtistsText()

    def getListItem(self):
        li = HasListItem.getListItem(self)
        if self.available:
            url = plugin.url_for_path('/play_video/%s' % self.id)
            isFolder = False
        else:
            url = plugin.url_for_path('/stream_locked')
            isFolder = True
        infoLabel = {
            'artist': [self.artist.name],
            'title': self.title,
            'tracknumber': self._playlist_pos + 1 if self._playlist_id else self._itemPosition + 1,
            'year': getattr(self, 'year', None),
            'plotoutline': self.getComment(),
            'plot': self.getFtArtistsText()
        }
        musicLabel = {
            'artist': self.artist.name,
            'title': self.title,
            'tracknumber': self._playlist_pos + 1 if self._playlist_id else self._itemPosition + 1,
            'year': getattr(self, 'year', None),
            'comment': self.getComment()
        }
        try:
            if self.streamStartDate:
                infoLabel.update({'date': self.streamStartDate.date().strftime('%d.%m.%Y')})
                musicLabel.update({'date': self.streamStartDate.date().strftime('%d.%m.%Y')})
            elif self.releaseDate:
                infoLabel.update({'date': self.releaseDate.date().strftime('%d.%m.%Y')})
                musicLabel.update({'date': self.releaseDate.date().strftime('%d.%m.%Y')})
        except:
            pass
        if KODI_VERSION >= (17, 0):
            infoLabel.update({'mediatype': 'musicvideo',
                              'rating': '%s' % int(round(self.popularity / 10.0)),
                              'userrating': '%s' % int(round(self.popularity / 10.0))
                              })
        li.setInfo('video', infoLabel)
        li.setInfo('music', musicLabel)
        li.addStreamInfo('video', { 'codec': 'h264', 'aspect': 1.78, 'width': 1920,
                         'height': 1080, 'duration': self.duration })
        li.addStreamInfo('audio', { 'codec': 'AAC', 'language': 'en', 'channels': 2 })
        return (url, li, isFolder)

    def getContextMenuItems(self):
        cm = []
        if self._is_logged_in:
            if self._isFavorite:
                cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/videos/%s' % self.id)))
            else:
                cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/videos/%s' % self.id)))
            if self._playlist_type == 'USER':
                cm.append((_T(30240), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove/%s/%s' % (self._playlist_id, self._playlist_pos))))
                cm.append((_T(30248), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/move/%s/%s/%s' % (self._playlist_id, self._playlist_pos, self.id))))
            else:
                cm.append((_T(30239), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/add/video/%s' % self.id)))
            plids = self._userplaylists.keys()
            for plid in plids:
                if plid <> self._playlist_id:
                    cm.append(((_T(30247).format(name=self._userplaylists[plid].get('title')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove_id/%s/%s' % (plid, self.id)))))
        cm.append((_T(30221), 'Container.Update(%s)' % plugin.url_for_path('/artist/%s' % self.artist.id)))
        cm.append((_T(30224), 'Container.Update(%s)' % plugin.url_for_path('/recommended/videos/%s' % self.id)))
        return cm


class PromotionItem(Promotion, HasListItem):

    def __init__(self, item):
        if item.type != 'EXTURL' and item.id.startswith('http:'):
            item.type = 'EXTURL' # Fix some defect TIDAL Promotions
        self.__dict__.update(vars(item))
        self._userplaylists = {} # Filled by parser

    def getLabel(self, extended=True):
        self.setLabelFormat()
        if self.type in ['ALBUM', 'VIDEO']:
            label = '%s - %s' % (self.shortHeader, self.shortSubHeader)
        else:
            label = self.shortHeader
        if extended and self._isFavorite:
            label = self.FAVORITE_MASK.format(label=label)
        txt = []
        plids = self._userplaylists.keys()
        for plid in plids:
            txt.append('%s' % self._userplaylists.get(plid).get('title'))
        if extended and txt:
            label = self.USER_PLAYLIST_MASK.format(label=label, userpl=', '.join(txt))
        return label

    def getListItem(self):
        li = HasListItem.getListItem(self)
        isFolder = True
        if self.type == 'PLAYLIST':
            url = plugin.url_for_path('/playlist/%s/items/0' % self.id)
            infoLabel = {
                'artist': self.shortHeader,
                'album': self.text,
                'title': self.shortSubHeader
            }
            if KODI_VERSION >= (17, 0):
                infoLabel.update({'userrating': '%s' % int(round(self.popularity / 10.0))})
            li.setInfo('music', infoLabel)
        elif self.type == 'ALBUM':
            url = plugin.url_for_path('/album/%s' % self.id)
            infoLabel = {
                'artist': self.shortHeader,
                'album': self.text,
                'title': self.shortSubHeader
            }
            if KODI_VERSION >= (17, 0):
                infoLabel.update({'mediatype': 'album',
                                  'userrating': '%s' % int(round(self.popularity / 10.0))
                                  })
            li.setInfo('music', infoLabel)
        elif self.type == 'VIDEO':
            url = plugin.url_for_path('/play_video/%s' % self.id)
            infoLabel = {
                'artist': [self.shortHeader],
                'album': self.text,
                'title': self.shortSubHeader
            }
            if KODI_VERSION >= (17, 0):
                infoLabel.update({'mediatype': 'musicvideo',
                                  'userrating': '%s' % int(round(self.popularity / 10.0))
                                  })
            li.setInfo('video', infoLabel)
            li.setProperty('isplayable', 'true')
            isFolder = False
            li.addStreamInfo('video', { 'codec': 'h264', 'aspect': 1.78, 'width': 1920,
                             'height': 1080, 'duration': self.duration })
            li.addStreamInfo('audio', { 'codec': 'AAC', 'language': 'en', 'channels': 2 })
        else:
            return (None, None, False)
        return (url, li, isFolder)

    def getContextMenuItems(self):
        cm = []
        if self.type == 'PLAYLIST':
            if self._is_logged_in:
                if self._isFavorite:
                    cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/playlists/%s' % self.id)))
                else:
                    cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/playlists/%s' % self.id)))
            cm.append((_T(30255), 'Container.Update(%s)' % plugin.url_for_path('/playlist/%s/albums/0' % self.id)))
        elif self.type == 'ALBUM':
            if self._is_logged_in:
                if self._isFavorite:
                    cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/albums/%s' % self.id)))
                else:
                    cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/albums/%s' % self.id)))
        elif self.type == 'VIDEO':
            if self._is_logged_in:
                if self._isFavorite:
                    cm.append((_T(30220), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/remove/videos/%s' % self.id)))
                else:
                    cm.append((_T(30219), 'RunPlugin(%s)' % plugin.url_for_path('/favorites/add/videos/%s' % self.id)))
                cm.append((_T(30239), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/add/video/%s' % self.id)))
            plids = self._userplaylists.keys()
            for plid in plids:
                cm.append(((_T(30247).format(name=self._userplaylists[plid].get('title')), 'RunPlugin(%s)' % plugin.url_for_path('/user_playlist/remove_id/%s/%s' % (plid, self.id)))))
            cm.append((_T(30224), 'Container.Update(%s)' % plugin.url_for_path('/recommended/videos/%s' % self.id)))
        return cm


class CategoryItem(Category, HasListItem):

    _force_subfolders = False
    _label = None

    def __init__(self, item):
        self.__dict__.update(vars(item))

    def getLabel(self, extended=True):
        self.setLabelFormat()
        if extended:
            return self.FOLDER_MASK.format(label=self._label)
        return self._label

    def getListItems(self):
        content_types = self.content_types
        items = []
        if len(content_types) > 1 and self._group in ['moods', 'genres'] and not self._force_subfolders:
            # Use sub folders for multiple Content Types
            url = plugin.url_for_path('/category/%s/%s' % (self._group, self.path))
            self._label = _P(self.path, self.name)
            li = HasListItem.getListItem(self)
            li.setInfo('music', {
                'artist': self._label
            })
            items.append((url, li, True))
        else:
            for content_type in content_types:
                url = plugin.url_for_path('/category/%s/%s/%s/%s' % (self._group, self.path, content_type, 0))
                if len(content_types) > 1:
                    if self._force_subfolders:
                        # Show only Content Type as sub folders
                        self._label = _P(content_type)
                    else:
                        # Show Path and Content Type as sub folder
                        self._label = '%s %s' % (_P(self.path, self.name), _P(content_type))
                else:
                    # Use Path as folder because content type is shows as sub foldes
                    self._label = _P(self.path, self.name)
                li = HasListItem.getListItem(self)
                li.setInfo('music', {
                    'artist': _P(self.path, self.name),
                    'album': _P(content_type)
                })
                items.append((url, li, True))
        return items


class FolderItem(BrowsableMedia, HasListItem):

    def __init__(self, label, url, thumb=None, fanart=None, isFolder=True, otherLabel=None):
        self.name = label
        self._url = url
        self._thumb = thumb
        self._fanart = fanart
        self._isFolder = isFolder
        self._otherLabel = otherLabel

    def getLabel(self, extended=True):
        self.setLabelFormat()
        label = self._otherLabel if self._otherLabel else self.name
        if extended:
            label = self.FOLDER_MASK.format(label=label)
        return label

    def getListItem(self):
        li = HasListItem.getListItem(self)
        li.setInfo('music', {
            'artist': self.name
        })
        return (self._url, li, self._isFolder)

    @property
    def image(self):
        return self._thumb

    @property
    def fanart(self):
        return self._fanart


class LoginToken(object):

    browser =   'wdgaB1CilGA-S_s2' # Streams HIGH/LOW Quality over RTMP, FLAC and Videos over HTTP, but many Lossless Streams are encrypted.
    browser2 =  'CzET4vdadNUFQ5JU' # All Streams encrypted (widevine ?)
    android =   'kgsOOmYk3zShYrNP' # All Streams are HTTP Streams. Correct numberOfVideos in Playlists (best Token to use)
    ios =       '_DSTon1kC8pABnTw' # Same as Android Token, but uses ALAC instead of FLAC
    native =    '4zx46pyr9o8qZNRw' # Same as Android Token, but FLAC streams are encrypted
    audirvana = 'MbjR4DLXz1ghC4rV' # Like Android Token, supports MQA, but returns 'numberOfVideos = 0' in Playlists
    amarra =    'wc8j_yBJd20zOmx0' # Like Android Token, but returns 'numberOfVideos = 0' in Playlists
    # Unkown working Tokens
    token1 =    'P5Xbeo5LFvESeDy6' # Like Android Token, but returns 'numberOfVideos = 0' in Playlists
    token2 =    'oIaGpqT_vQPnTr0Q' # Like token1, but uses RTMP for HIGH/LOW Quality
    token3 =    '_KM2HixcUBZtmktH' # Same as token1

    features = {
        # token: Login-Token to get a Session-ID
        # codecs: Supported Audio Codecs without encryption
        # rtmp: Uses RTMP Protocol for HIGH/LOW Quality Audio Streams
        # videosInPlaylists: True: numberOfVideos in Playlists is correct, False: returns 'numberOfVideos = 0' in Playlists
        # user-agent: Special User-Agent in HTTP-Request-Header
        'browser':   { 'token': browser,   'codecs': ['AAC'],                'rtmp': True,  'videoMode': 'HLS', 'videosInPlaylists': True,  'user-agent': None },
        'android':   { 'token': android,   'codecs': ['AAC', 'FLAC'],        'rtmp': False, 'videoMode': 'HTTP','videosInPlaylists': True,  'user-agent': 'TIDAL_ANDROID/686 okhttp/3.3.1' },
        'ios':       { 'token': ios,       'codecs': ['AAC', 'ALAC'],        'rtmp': False, 'videoMode': 'HLS', 'videosInPlaylists': True,  'user-agent': 'TIDAL/546 CFNetwork/808.2.16 Darwin/16.3.0' },
        'native':    { 'token': native,    'codecs': ['AAC'],                'rtmp': False, 'videoMode': 'HLS', 'videosInPlaylists': True,  'user-agent': 'TIDAL_NATIVE_PLAYER/OSX/2.3.20' },
        'audirvana': { 'token': audirvana, 'codecs': ['AAC', 'FLAC', 'MQA'], 'rtmp': False, 'videoMode': 'HLS', 'videosInPlaylists': False, 'user-agent': 'Audirvana/3550 CFNetwork/897.15 Darwin/17.5.0 (x86_64)' },
        'amarra':    { 'token': amarra,    'codecs': ['AAC', 'FLAC'],        'rtmp': False, 'videoMode': 'HLS', 'videosInPlaylists': False, 'user-agent': 'Amarra for TIDAL/2.2.1261 CFNetwork/807.2.14 Darwin/16.3.0 (x86_64)' },
        # Unknown working Tokens
        'token1':    { 'token': token1,    'codecs': ['AAC', 'FLAC'],        'rtmp': False, 'videoMode': 'HTTP','videosInPlaylists': False, 'user-agent': None },
        'token2':    { 'token': token2,    'codecs': ['AAC', 'FLAC'],        'rtmp': True,  'videoMode': 'HLS', 'videosInPlaylists': False, 'user-agent': None },
        'token3':    { 'token': token3,    'codecs': ['AAC', 'FLAC'],        'rtmp': False, 'videoMode': 'HTTP','videosInPlaylists': False, 'user-agent': None }
    }

    priority = ['android', 'ios', 'audirvana', 'browser', 'native', 'amarra', 'token1', 'token2', 'token3']

    @staticmethod
    def getFeatures(tokenName='android'):
        return LoginToken.features.get(tokenName, None)

    @staticmethod
    def getToken(tokenName='android'):
        return LoginToken.getFeatures(tokenName).get('token')

    @staticmethod
    def select(codec, rtmp=False, api=True, forceHttpVideo=False):
        primary_tokens = []
        secondary_tokens = []
        lossless = codec in ['FLAC', 'ALAC', 'MQA']
        rtmp_relevant = False if api or lossless else True
        video_modes = ['HTTP'] if api and forceHttpVideo else ['HLS', 'HTTP'] if api or forceHttpVideo else ['HLS']
        for tokenName in LoginToken.priority:
            token = LoginToken.getFeatures(tokenName)
            if codec in token.get('codecs') and \
               (token.get('videoMode') in video_modes) and \
               (not rtmp_relevant or token.get('rtmp') == rtmp) and \
               (not api or token.get('videosInPlaylists') == api):
                if api and token.get('videoMode') == 'HTTP' and not forceHttpVideo:
                    secondary_tokens.append(tokenName)
                else:
                    primary_tokens.append(tokenName)
        tokens = primary_tokens + secondary_tokens
        if not tokens:
            log('No Token found for Codec:%s, RTMP:%s, API:%s, HTTP:%s' % (codec, rtmp, api, forceHttpVideo) )
        return tokens


# Session from the TIDAL-API to parse Items into Kodi List Items

class TidalConfig(Config):

    def __init__(self):
        Config.__init__(self)
        self.load()

    def load(self):
        self.session_id = addon.getSetting('session_id')
        self.session_token_name = addon.getSetting('session_token_name')
        self.stream_session_id = addon.getSetting('stream_session_id')
        self.stream_token_name = addon.getSetting('stream_token_name')
        if not self.stream_session_id:
            self.stream_session_id = self.session_id
            self.stream_token_name = self.session_token_name
        self.country_code = addon.getSetting('country_code')
        # Determine the locale of the system
        self.locale = None
        try:
            self.locale = locale.getdefaultlocale()[0]
        except:
            pass
        if not self.locale:
            try:
                self.locale = locale.getlocale()[0]
            except:
                pass
        if not self.locale:
            try:
                langval = json.loads(xbmc.executeJSONRPC('{ "jsonrpc": "2.0", "method": "Settings.GetSettingValue", "params": {"setting": "locale.language"}, "id": 1 }'))['result']['value'].split('.')[-1]
                self.locale = locale.locale_alias.get(langval).split('.')[0]
            except:
                pass
        if not self.locale:
            # If no locale is found take the US locale
            self.locale = 'en_US'
        self.user_id = addon.getSetting('user_id')
        self.subscription_type = [SubscriptionType.hifi, SubscriptionType.premium][min(1, int('0' + addon.getSetting('subscription_type')))]
        self.client_unique_key = addon.getSetting('client_unique_key')
        self.quality = [Quality.lossless, Quality.high, Quality.low][min(2, int('0' + addon.getSetting('quality')))]
        self.use_rtmp = True if addon.getSetting('music_option') == '3' and self.quality <> Quality.lossless else False
        self.codec = ['FLAC', 'AAC', 'AAC'][min([2, int('0' + addon.getSetting('quality'))])]
        if addon.getSetting('music_option') == '1' and self.quality == Quality.lossless:
            self.codec = 'ALAC'
        elif addon.getSetting('music_option') == '2' and self.quality == Quality.lossless:
            self.codec = 'MQA'
        self.maxVideoHeight = [9999, 1080, 720, 540, 480, 360, 240][min(6, int('0%s' % addon.getSetting('video_quality')))]
        self.pageSize = max(10, min(9999, int('0%s' % addon.getSetting('page_size'))))
        self.debug = True if addon.getSetting('debug_log') == 'true' else False
        self.debug_json = True if addon.getSetting('debug_json') == 'true' else False
        self.forceHttpVideo = True if addon.getSetting('http_for_videos') == 'true' else False
        self.http_video_session_id = self.stream_session_id
        if self.forceHttpVideo:
            apiFeatures = LoginToken.getFeatures(self.session_token_name)
            if apiFeatures and apiFeatures.get('videoMode') == 'HTTP':
                self.http_video_session_id = self.session_id

class TidalSession(Session):

    errorCodes = []

    def __init__(self, config=TidalConfig()):
        Session.__init__(self, config=config)

    def halt(self):
        debug.halt()

    def init_user(self, user_id, subscription_type):
        return TidalUser(self, user_id, subscription_type)

    def load_session(self):
        if not self._config.country_code:
            self._config.country_code = self.local_country_code()
            addon.setSetting('country_code', self._config.country_code)
        Session.load_session(self, self._config.session_id, self._config.country_code, self._config.user_id,
                             self._config.subscription_type, self._config.client_unique_key)
        self.stream_session_id = self._config.stream_session_id
        self.http_video_session_id = self._config.http_video_session_id

    def generate_client_unique_key(self):
        unique_key = addon.getSetting('client_unique_key')
        if not unique_key:
            unique_key = Session.generate_client_unique_key(self)
        return unique_key

    def login_with_token(self, username, password, subscription_type, tokenName, api=True):
        old_token = self._config.api_token
        old_session_id = self.session_id
        self._config.api_token = LoginToken.getToken(tokenName)
        self.session_id = None
        Session.login(self, username, password, subscription_type)
        success = True if self.session_id else False
        if not api:
            self.stream_session_id = self.session_id
            if old_session_id:
                self.session_id = old_session_id
        self._config.api_token = old_token
        return success

    def login(self, username, password, subscription_type=None):
        if not username or not password:
            return False
        if not subscription_type:
            # Set Subscription Type corresponding to the given playback quality
            subscription_type = SubscriptionType.hifi if self._config.quality == Quality.lossless else SubscriptionType.premium
        if not self.client_unique_key:
            # Generate a random client key if no key is given
            self.client_unique_key = self.generate_client_unique_key()
        api_token = ''
        stream_token = ''
        # Get working Tokens with correct numberOfVideos in Playlists which can be used for API calls and for Streaming
        tokenNames = LoginToken.select(codec=self._config.codec, rtmp=self._config.use_rtmp, api=True, forceHttpVideo=self._config.forceHttpVideo)
        if not tokenNames:
            # Get a default API Token
            tokenNames = LoginToken.select(codec='AAC', rtmp=self._config.use_rtmp, api=True, forceHttpVideo=self._config.forceHttpVideo)
        # if not tokenNames:
        for tokenName in tokenNames:
            log('Try Login for API Session with %s Token %s ...' % (tokenName, LoginToken.getToken(tokenName)))
            loginOk = self.login_with_token(username, password, subscription_type, tokenName, api=True)
            if loginOk:
                api_token = tokenName
                # Use the API Session also for Streaming as default
                stream_token = api_token
                self.stream_session_id = self.session_id
                break
        # Get Tokens which are necessary for Streaming
        tokenNames = LoginToken.select(codec=self._config.codec, rtmp=self._config.use_rtmp, api=False, forceHttpVideo=False)
        if api_token in tokenNames:
            log('Using API Session also for Streaming.')
        else:
            # Get Session-ID for Streaming
            for tokenName in tokenNames:
                log('Try Login for Stream Session with %s Token %s ...' % (tokenName, LoginToken.getToken(tokenName)))
                loginOk = self.login_with_token(username, password, subscription_type, tokenName, api=False)
                if loginOk:
                    stream_token = tokenName
                    break
        # Save Session Data into Addon-Settings
        if self.is_logged_in:
            addon.setSetting('session_id', self.session_id)
            addon.setSetting('session_token_name', api_token)
            addon.setSetting('stream_session_id', self.stream_session_id)
            addon.setSetting('stream_token_name', stream_token)
            addon.setSetting('country_code', self.country_code)
            addon.setSetting('user_id', unicode(self.user.id))
            addon.setSetting('subscription_type', '0' if self.user.subscription.type == SubscriptionType.hifi else '1')
            addon.setSetting('client_unique_key', self.client_unique_key)
            # Reload the Configuration after Settings are saved.
            self._config.load()
            self.load_session()
            if self._config.forceHttpVideo and self.http_video_session_id == self.session_id and self.session_id <> self.stream_session_id:
                log('Using API Session for HTTP Video Streaming. Videos are limited to 720p !')
        return self.is_logged_in

    def logout(self):
        Session.logout(self)
        self.stream_session_id = None
        addon.setSetting('session_id', '')
        addon.setSetting('stream_session_id', '')
        addon.setSetting('user_id', '')
        self._config.load()

    def get_album_tracks(self, album_id, withAlbum=True):
        items = Session.get_album_tracks(self, album_id)
        if withAlbum:
            album = self.get_album(album_id)
            if album:
                for item in items:
                    item.album = album
        return items

    def get_playlist_tracks(self, playlist_id, offset=0, limit=9999):
        # keeping 1st parameter as playlist_id for backward compatibility 
        if isinstance(playlist_id, Playlist):
            playlist = playlist_id
            playlist_id = playlist.id
        else:
            playlist = self.get_playlist(playlist_id)
        # Don't read empty playlists
        if not playlist or playlist.numberOfItems == 0:
            return []
        items = Session.get_playlist_tracks(self, playlist.id, offset=offset, limit=limit)
        if items:
            for item in items:
                item._etag = playlist._etag
                item._playlist_name = playlist.title
                item._playlist_type = playlist.type
        return items

    def get_item_albums(self, items):
        albums = []
        for item in items:
            album = item.album
            if not album.releaseDate:
                album.releaseDate = item.streamStartDate
            # Item-Position in the Kodi-List (filled by _map_request)
            album._itemPosition = item._itemPosition
            album._offset = item._offset
            album._totalNumberOfItems = item._totalNumberOfItems
            # Infos for Playlist-Item-Position (filled by get_playlist_tracks, get_playlist_items)
            album._playlist_id = item._playlist_id
            album._playlist_pos = item._playlist_pos
            album._etag = item._etag
            album._playlist_name = item._playlist_name
            album._playlist_type = item._playlist_type
            album._userplaylists = self.user.playlists_of_id(None, album.id)
            # Track-ID in TIDAL-Playlist
            album._playlist_track_id = item.id
            # Album Quality = Track Quality if Album Cache is disabled
            if not self._config.cache_albums:
                album.audioQuality = item.audioQuality
            albums.append(album)
        return albums

    def get_playlist_albums(self, playlist, offset=0, limit=9999):
        return self.get_item_albums(self.get_playlist_tracks(self, playlist, offset=offset, limit=limit))

    def get_artist_top_tracks(self, artist_id, offset=0, limit=999):
        items = Session.get_artist_top_tracks(self, artist_id, offset=offset, limit=limit)
        if not items and limit >= 100:
            items = Session.get_artist_top_tracks(self, artist_id, offset=offset, limit=100)
        if not items and limit >= 50:
            items = Session.get_artist_top_tracks(self, artist_id, offset=offset, limit=50)
        if not items:
            items = Session.get_artist_top_tracks(self, artist_id, offset=offset, limit=20)
        return items

    def get_artist_radio(self, artist_id, offset=0, limit=999):
        items = Session.get_artist_radio(self, artist_id, offset=offset, limit=limit)
        if not items and limit >= 100:
            items = Session.get_artist_radio(self, artist_id, offset=offset, limit=100)
        if not items and limit >= 50:
            items = Session.get_artist_radio(self, artist_id, offset=offset, limit=50)
        if not items:
            items = Session.get_artist_radio(self, artist_id, offset=offset, limit=20)
        return items

    def get_track_radio(self, track_id, offset=0, limit=999):
        items = Session.get_track_radio(self, track_id, offset=offset, limit=limit)
        if not items and limit >= 100:
            items = Session.get_track_radio(self, track_id, offset=offset, limit=100)
        if not items and limit >= 50:
            items = Session.get_track_radio(self, track_id, offset=offset, limit=50)
        if not items:
            items = Session.get_track_radio(self, track_id, offset=offset, limit=20)
        return items

    def get_recommended_items(self, content_type, item_id, offset=0, limit=999):
        items = Session.get_recommended_items(self, content_type, item_id, offset=offset, limit=limit)
        if not items and limit >= 100:
            items = Session.get_recommended_items(self, content_type, item_id, offset=offset, limit=100)
        if not items and limit >= 50:
            items = Session.get_recommended_items(self, content_type, item_id, offset=offset, limit=50)
        if not items:
            items = Session.get_recommended_items(self, content_type, item_id, offset=offset, limit=20)
        return items

    def _parse_album(self, json_obj, artist=None):
        album = AlbumItem(Session._parse_album(self, json_obj, artist=artist))
        album._is_logged_in = self.is_logged_in
        if self.is_logged_in:
            album._userplaylists = self.user.playlists_of_id(None, album.id)
        return album

    def _parse_artist(self, json_obj):
        artist = ArtistItem(Session._parse_artist(self, json_obj))
        if self.is_logged_in and self.user.favorites:
            artist._isLocked = self.user.favorites.isLockedArtist(artist.id)
        artist._is_logged_in = self.is_logged_in
        return artist

    def _parse_mix(self, json_obj):
        mix = MixItem(Session._parse_mix(self, json_obj))
        mix._is_logged_in = self.is_logged_in
        return mix

    def _parse_playlist(self, json_obj):
        playlist = PlaylistItem(Session._parse_playlist(self, json_obj))
        playlist._is_logged_in = self.is_logged_in
        return playlist

    def _parse_track(self, json_obj):
        track = TrackItem(Session._parse_track(self, json_obj))
        if not getattr(track.album, 'streamStartDate', None):
            track.album.streamStartDate = track.streamStartDate
        track.album.explicit = track.explicit
        track._is_logged_in = self.is_logged_in
        if self.is_logged_in:
            track._userplaylists = self.user.playlists_of_id(track.id, track.album.id)
        elif track.duration > 30:
            # 30 Seconds Limit in Trial Mode
            track.duration = 30
        return track

    def _parse_video(self, json_obj):
        video = VideoItem(Session._parse_video(self, json_obj))
        video._is_logged_in = self.is_logged_in
        if self.is_logged_in:
            video._userplaylists = self.user.playlists_of_id(video.id)
        elif video.duration > 30:
            # 30 Seconds Limit in Trial Mode
            video.duration = 30
        return video

    def _parse_promotion(self, json_obj):
        promotion = PromotionItem(Session._parse_promotion(self, json_obj))
        promotion._is_logged_in = self.is_logged_in
        if self.is_logged_in and promotion.type == 'VIDEO':
            promotion._userplaylists = self.user.playlists_of_id(promotion.id)
        return promotion

    def _parse_category(self, json_obj):
        return CategoryItem(Session._parse_category(self, json_obj))

    def get_media_url(self, track_id, quality=None, cut_id=None, fallback=False):
        return Session.get_media_url(self, track_id, quality=quality, cut_id=cut_id, fallback=fallback)

    def get_track_url(self, track_id, quality=None, cut_id=None, fallback=True):
        oldSessionId = self.session_id
        self.session_id = self.stream_session_id
        soundQuality = quality if quality else self._config.quality
        #if soundQuality == Quality.lossless and self._config.codec == 'MQA' and not cut_id:
        #    soundQuality = Quality.hi_res
        media = Session.get_track_url(self, track_id, quality=soundQuality, cut_id=cut_id)
        if fallback and soundQuality == Quality.lossless and (media == None or media.isEncrypted):
            log(media.url, level=xbmc.LOGWARNING)
            if media:
                log('Got encryptionKey "%s" for track %s, trying HIGH Quality ...' % (media.encryptionKey, track_id), level=xbmc.LOGWARNING)
            else:
                log('No Lossless stream for track %s, trying HIGH Quality ...' % track_id, level=xbmc.LOGWARNING)
            media = self.get_track_url(track_id, quality=Quality.high, cut_id=cut_id, fallback=False)
        if media:
            if quality == Quality.lossless and media.codec not in ['FLAC', 'ALAC', 'MQA']:
                xbmcgui.Dialog().notification(plugin.name, _T(30504) , icon=xbmcgui.NOTIFICATION_WARNING)
            log('Got stream with soundQuality:%s, codec:%s' % (media.soundQuality, media.codec))
        self.session_id = oldSessionId
        return media

    def get_video_url(self, video_id, maxHeight=-1):
        oldSessionId = self.session_id
        self.session_id = self.http_video_session_id
        maxVideoHeight = maxHeight if maxHeight > 0 else self._config.maxVideoHeight
        media = None
        try:
            if self._config.forceHttpVideo:
                quality = 'LOW' if self._config.maxVideoHeight < 480 else 'MEDIUM' if self._config.maxVideoHeight < 720 else 'HIGH'
                media = Session.get_video_url(self, video_id, quality=quality)
        except requests.HTTPError as e:
            r = e.response
            msg = _T(30505)
            try:
                msg = r.reason
                msg = r.json().get('userMessage')
            except:
                pass
            log('HTTP-Error: ' + msg, xbmc.LOGERROR)
            log('Got no HTTP Stream for Video ID %s, using HLS Stream ...' % video_id, xbmc.LOGERROR)
            xbmcgui.Dialog().notification(plugin.name, _T(30510), xbmcgui.NOTIFICATION_WARNING)
        if not media:
            # Using HLS-Stream 
            self.session_id = self.stream_session_id
            media = Session.get_video_url(self, video_id, quality=None)
        if maxVideoHeight <> 9999 and media.url.lower().find('.m3u8') > 0:
            log('Parsing M3U8 Playlist: %s' % media.url)
            m3u8obj = m3u8_load(media.url)
            if m3u8obj.is_variant and not m3u8obj.cookies:
                # Variant Streams with Cookies have to be played without stream selection.
                # You can change the Bandwidth Limit in Kodi Settings to select other streams !
                # Select stream with highest resolution <= maxVideoHeight
                selected_height = 0
                selected_bandwidth = -1
                for playlist in m3u8obj.playlists:
                    try:
                        width, height = playlist.stream_info.resolution
                        bandwidth = playlist.stream_info.average_bandwidth
                        if not bandwidth:
                            bandwidth = playlist.stream_info.bandwidth
                        if not bandwidth:
                            bandwidth = 0
                        if (height > selected_height or (height == selected_height and bandwidth > selected_bandwidth)) and height <= maxVideoHeight:
                            if re.match(r'https?://', playlist.uri):
                                media.url = playlist.uri
                            else:
                                media.url = m3u8obj.base_uri + playlist.uri
                            if height == selected_height and bandwidth > selected_bandwidth:
                                log('Bandwidth %s > %s' % (bandwidth, selected_bandwidth))
                            log('Selected %sx%s %s: %s' % (width, height, bandwidth, playlist.uri.split('?')[0].split('/')[-1]))
                            selected_height = height
                            selected_bandwidth = bandwidth
                            media.width = width
                            media.height = height
                            media.bandwidth = bandwidth
                        elif height > maxVideoHeight:
                            log('Skipped %sx%s %s: %s' % (width, height, bandwidth, playlist.uri.split('?')[0].split('/')[-1]))
                    except:
                        pass
        self.session_id = oldSessionId
        return media

    def add_list_items(self, items, content=None, end=True, withNextPage=False):
        if content:
            xbmcplugin.setContent(plugin.handle, content)
        list_items = []
        for item in items:
            if isinstance(item, Category):
                category_items = item.getListItems()
                for url, li, isFolder in category_items:
                    if url and li:
                        list_items.append(('%s/' % url if isFolder else url, li, isFolder))
            elif isinstance(item, BrowsableMedia):
                url, li, isFolder = item.getListItem()
                if url and li:
                    list_items.append(('%s/' % url if isFolder else url, li, isFolder))
        if withNextPage and len(items) > 0:
            # Add folder for next page
            try:
                totalNumberOfItems = items[0]._totalNumberOfItems
                nextOffset = items[0]._offset + self._config.pageSize
                if nextOffset < totalNumberOfItems and len(items) >= self._config.pageSize:
                    path = urlsplit(sys.argv[0]).path or '/'
                    path = path.split('/')[:-1]
                    path.append(str(nextOffset))
                    url = '/'.join(path)
                    self.add_directory_item(_T(30244).format(pos1=nextOffset, pos2=min(nextOffset+self._config.pageSize, totalNumberOfItems)), plugin.url_for_path(url))
            except:
                log('Next Page for URL %s not set' % sys.argv[0], xbmc.LOGERROR)
        if len(list_items) > 0:
            xbmcplugin.addDirectoryItems(plugin.handle, list_items)
        if end:
            xbmcplugin.endOfDirectory(plugin.handle)

    def add_directory_item(self, title, endpoint, thumb=None, fanart=None, end=False, isFolder=True):
        if callable(endpoint):
            endpoint = plugin.url_for(endpoint)
        item = FolderItem(title, endpoint, thumb, fanart, isFolder)
        self.add_list_items([item], end=end)


class TidalFavorites(Favorites):

    def __init__(self, session, user_id):
        Favorites.__init__(self, session, user_id)

    def load_cache(self):
        try:
            fd = xbmcvfs.File(FAVORITES_FILE, 'r')
            self.ids_content = fd.read()
            self.ids = eval(self.ids_content)
            if not 'locked_artists' in self.ids:
                self.ids['locked_artists'] = [VARIOUS_ARTIST_ID]
            fd.close()
            self.ids_loaded = not (self.ids['artists'] == None or self.ids['albums'] == None or
                                   self.ids['playlists'] == None or self.ids['tracks'] == None or
                                   self.ids['videos'] == None)
            if self.ids_loaded:
                log('Loaded %s Favorites from disk.' % sum(len(self.ids[content]) for content in ['artists', 'albums', 'playlists', 'tracks', 'videos']))
        except:
            self.ids_loaded = False
            self.reset()
        return self.ids_loaded

    def save_cache(self):
        try:
            if self.ids_loaded:
                new_ids = repr(self.ids)
                if new_ids <> self.ids_content:
                    fd = xbmcvfs.File(FAVORITES_FILE, 'w')
                    fd.write(new_ids)
                    fd.close()
                    log('Saved %s Favorites to disk.' % sum(len(self.ids[content]) for content in ['artists', 'albums', 'playlists', 'tracks', 'videos']))
        except:
            return False
        return True

    def delete_cache(self):
        try:
            if xbmcvfs.exists(FAVORITES_FILE):
                xbmcvfs.delete(FAVORITES_FILE)
                log('Deleted Favorites file.')
        except:
            return False
        return True

    def load_all(self, force_reload=False):
        if not force_reload and self.ids_loaded:
            return self.ids
        if not force_reload:
            self.load_cache()
        if force_reload or not self.ids_loaded:
            Favorites.load_all(self, force_reload=force_reload)
            self.save_cache()
        return self.ids

    def get(self, content_type, limit=9999):
        items = Favorites.get(self, content_type, limit=limit)
        if items:
            self.load_all()
            self.ids[content_type] = sorted(['%s' % item.id for item in items])
            self.save_cache()
        return items

    def add(self, content_type, item_ids):
        ok = Favorites.add(self, content_type, item_ids)
        if ok:
            self.get(content_type)
        return ok

    def remove(self, content_type, item_id):
        ok = Favorites.remove(self, content_type, item_id)
        if ok:
            self.get(content_type)
        return ok

    def isFavoriteArtist(self, artist_id):
        self.load_all()
        return Favorites.isFavoriteArtist(self, artist_id)

    def isFavoriteAlbum(self, album_id):
        self.load_all()
        return Favorites.isFavoriteAlbum(self, album_id)

    def isFavoritePlaylist(self, playlist_id):
        self.load_all()
        return Favorites.isFavoritePlaylist(self, playlist_id)

    def isFavoriteTrack(self, track_id):
        self.load_all()
        return Favorites.isFavoriteTrack(self, track_id)

    def isFavoriteVideo(self, video_id):
        self.load_all()
        return Favorites.isFavoriteVideo(self, video_id)

    def isLockedArtist(self, artist_id):
        self.load_all()
        return '%s' % artist_id in self.ids.get('locked_artists', [])

    def setLockedArtist(self, artist_id, lock=True):
        self.load_all()
        actually_locked = self.isLockedArtist(artist_id)
        ok = True
        if lock <> actually_locked:
            try:
                if lock:
                    self.ids['locked_artists'].append('%s' % artist_id)
                    self.ids['locked_artists'] = sorted(self.ids['locked_artists'])
                else:
                    self.ids['locked_artists'].remove('%s' % artist_id)
                self.save_cache()
            except:
                ok = False
        return ok


class TidalUser(User):

    def __init__(self, session, user_id, subscription_type=SubscriptionType.hifi):
        User.__init__(self, session, user_id, subscription_type)
        self.favorites = TidalFavorites(session, user_id)
        self.playlists_loaded = False
        self.playlists_cache = {}

    def load_cache(self):
        try:
            fd = xbmcvfs.File(PLAYLISTS_FILE, 'r')
            self.playlists_cache = eval(fd.read())
            fd.close()
            self.playlists_loaded = True
            log('Loaded %s Playlists from disk.' % len(self.playlists_cache.keys()))
        except:
            self.playlists_loaded = False
            self.playlists_cache = {}
        return self.playlists_loaded

    def save_cache(self):
        try:
            if self.playlists_loaded:
                fd = xbmcvfs.File(PLAYLISTS_FILE, 'w')
                fd.write(repr(self.playlists_cache))
                fd.close()
                log('Saved %s Playlists to disk.' % len(self.playlists_cache.keys()))
        except:
            return False
        return True

    def check_updated_playlist(self, playlist):
        if self.playlists_cache.get(playlist.id, {}).get('lastUpdated', datetime.datetime.fromordinal(1)) == playlist.lastUpdated:
            # Playlist unchanged
            return False
        #if playlist.numberOfVideos == 0:
        #    items = self._session.get_playlist_tracks(playlist)
        #else:
        items = self._session.get_playlist_items(playlist)
        album_ids = []
        if ALBUM_PLAYLIST_TAG in playlist.description:
            album_ids = ['%s' % item.album.id for item in items if isinstance(item, TrackItem)]
        # Save Track-IDs into Buffer
        self.playlists_cache.update({playlist.id: {'title': playlist.title,
                                                   'description': playlist.description,
                                                   'lastUpdated': playlist.lastUpdated,
                                                   'ids': ['%s' % item.id for item in items],
                                                   'album_ids': album_ids}})
        return True

    def delete_cache(self):
        try:
            if xbmcvfs.exists(PLAYLISTS_FILE):
                xbmcvfs.delete(PLAYLISTS_FILE)
                log('Deleted Playlists file.')
        except:
            return False
        return True

    def playlists_of_id(self, item_id, album_id=None):
        userpl = {}
        if not self.playlists_loaded:
            self.load_cache()
        if not self.playlists_loaded:
            self.playlists()
        plids = self.playlists_cache.keys()
        for plid in plids:
            if item_id and '%s' % item_id in self.playlists_cache.get(plid).get('ids', []):
                userpl.update({plid: self.playlists_cache.get(plid)})
            if album_id and '%s' % album_id in self.playlists_cache.get(plid).get('album_ids', []):
                userpl.update({plid: self.playlists_cache.get(plid)})
        return userpl

    def playlists(self):
        items = User.playlists(self, offset=0, limit=9999)
        # Refresh the Playlist Cache
        if not self.playlists_loaded:
            self.load_cache()
        buffer_changed = False
        act_ids = [item.id for item in items]
        saved_ids = self.playlists_cache.keys()
        # Remove Deleted Playlists from Cache
        for plid in saved_ids:
            if plid not in act_ids:
                self.playlists_cache.pop(plid)
                buffer_changed = True
        # Update modified Playlists in Cache
        self.playlists_loaded = True
        for item in items:
            if self.check_updated_playlist(item):
                buffer_changed = True
        if buffer_changed:
            self.save_cache()
        return items

    def add_playlist_entries(self, playlist=None, item_ids=[]):
        ok = User.add_playlist_entries(self, playlist=playlist, item_ids=item_ids)
        if ok:
            self.playlists()
        return ok

    def remove_playlist_entry(self, playlist, entry_no=None, item_id=None):
        ok = User.remove_playlist_entry(self, playlist, entry_no=entry_no, item_id=item_id)
        if ok:
            self.playlists()
        return ok

    def delete_playlist(self, playlist_id):
        ok = User.delete_playlist(self, playlist_id)
        if ok:
            self.playlists()
        return ok

    def renamePlaylistDialog(self, playlist):
        dialog = xbmcgui.Dialog()
        title = dialog.input(_T(30233), playlist.title, type=xbmcgui.INPUT_ALPHANUM)
        ok = False
        if title:
            description = dialog.input(_T(30234), playlist.description, type=xbmcgui.INPUT_ALPHANUM)
            ok = self.rename_playlist(playlist, title, description)
        return ok

    def newPlaylistDialog(self):
        dialog = xbmcgui.Dialog()
        title = dialog.input(_T(30233), type=xbmcgui.INPUT_ALPHANUM)
        item = None
        if title:
            description = dialog.input(_T(30234), type=xbmcgui.INPUT_ALPHANUM)
            item = self.create_playlist(title, description)
        return item

    def selectPlaylistDialog(self, headline=None, allowNew=False):
        if not self._session.is_logged_in:
            return None
        try:
            if not headline:
                headline = _T(30238)
            items = self.playlists()
            dialog = xbmcgui.Dialog()
            item_list = [item.title for item in items]
            if allowNew:
                item_list.append(_T(30237))
        except Exception, e:
            log(str(e), level=xbmc.LOGERROR)
            return None
        selected = dialog.select(headline, item_list)
        if selected >= len(items):
            item = self.newPlaylistDialog()
            return item
        elif selected >= 0:
            return items[selected]
        return None