2019-06-12 17:44:45 -05:00
|
|
|
# -*- coding: utf-8 -*-
|
2020-07-08 13:12:33 -05:00
|
|
|
""" Tidal service support for MusicDL.
|
|
|
|
this service allows users to choose between 3 different qualities. Low, high and lossless.
|
|
|
|
Lossless quality is only available to the account of tidal capable of playing such kind of audio files.
|
|
|
|
for low and high qualities, the file is transcoded to mp3 because Tidal gives us that as m4a.
|
|
|
|
"""
|
2019-06-12 17:44:45 -05:00
|
|
|
import logging
|
2019-06-21 14:47:24 -05:00
|
|
|
import webbrowser
|
|
|
|
import wx
|
2019-06-12 17:44:45 -05:00
|
|
|
import tidalapi
|
|
|
|
import config
|
|
|
|
from update.utils import seconds_to_string
|
2019-06-18 16:16:29 -05:00
|
|
|
from .import base
|
|
|
|
|
2020-07-08 13:12:33 -05:00
|
|
|
log = logging.getLogger("services.tidal")
|
2019-06-12 17:44:45 -05:00
|
|
|
|
2019-06-20 17:44:39 -05:00
|
|
|
class interface(base.baseInterface):
|
2019-06-12 17:44:45 -05:00
|
|
|
name = "tidal"
|
2019-06-18 16:16:29 -05:00
|
|
|
enabled = config.app["services"]["tidal"].get("enabled")
|
2020-07-08 13:12:33 -05:00
|
|
|
# This should not be enabled if credentials are not set in config.
|
2019-06-20 10:18:49 -05:00
|
|
|
if config.app["services"]["tidal"]["username"] == "" or config.app["services"]["tidal"]["password"] == "":
|
|
|
|
enabled = False
|
2019-06-12 17:44:45 -05:00
|
|
|
|
|
|
|
def __init__(self):
|
2019-06-20 17:44:39 -05:00
|
|
|
super(interface, self).__init__()
|
|
|
|
self.setup()
|
|
|
|
|
|
|
|
def setup(self):
|
2019-06-20 10:18:49 -05:00
|
|
|
# Assign quality or switch to high if not specified/not found.
|
2019-06-12 17:44:45 -05:00
|
|
|
if hasattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"]):
|
|
|
|
quality = getattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"])
|
|
|
|
else:
|
|
|
|
quality = tidalapi.Quality.high
|
2020-07-08 13:12:33 -05:00
|
|
|
# We need to instantiate a config object to pass quality settings.
|
2019-06-12 17:44:45 -05:00
|
|
|
_config = tidalapi.Config(quality=quality)
|
|
|
|
username = config.app["services"]["tidal"]["username"]
|
|
|
|
password = config.app["services"]["tidal"]["password"]
|
|
|
|
log.debug("Using quality: %s" % (quality,))
|
|
|
|
self.session = tidalapi.Session(config=_config)
|
|
|
|
self.session.login(username=username, password=password)
|
2019-06-20 17:44:39 -05:00
|
|
|
|
|
|
|
def get_file_format(self):
|
2020-07-08 13:12:33 -05:00
|
|
|
""" Returns the file format (mp3 or flac) depending in quality set. """
|
2019-06-13 17:41:05 -05:00
|
|
|
if config.app["services"]["tidal"]["quality"] == "lossless":
|
|
|
|
self.file_extension = "flac"
|
|
|
|
else:
|
|
|
|
self.file_extension = "mp3"
|
2019-06-24 12:45:09 -05:00
|
|
|
return self.file_extension
|
2019-06-12 17:44:45 -05:00
|
|
|
|
2019-06-20 17:44:39 -05:00
|
|
|
def transcoder_enabled(self):
|
2020-07-08 13:12:33 -05:00
|
|
|
# If quality is set to high, tidal returns audio in AAC format at 256 KBPS. So we convert it with vlc to mp3 at 320KBPS.
|
|
|
|
# toDo: Shall this be a setting and allow MusicDL to spit out the m4a file directly?
|
2019-06-20 17:44:39 -05:00
|
|
|
if config.app["services"]["tidal"]["quality"] == "lossless":
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
2019-06-12 17:44:45 -05:00
|
|
|
def search(self, text, page=1):
|
|
|
|
if text == "" or text == None:
|
|
|
|
raise ValueError("Text must be passed and should not be blank.")
|
|
|
|
log.debug("Retrieving data from Tidal...")
|
|
|
|
fieldtypes = ["artist", "album", "playlist"]
|
2019-06-12 22:43:20 -05:00
|
|
|
field = "track"
|
2019-06-12 17:44:45 -05:00
|
|
|
for i in fieldtypes:
|
|
|
|
if text.startswith(i+"://"):
|
|
|
|
field = i
|
|
|
|
text = text.replace(i+"://", "")
|
|
|
|
log.debug("Searching for %s..." % (field))
|
|
|
|
search_response = self.session.search(value=text, field=field)
|
|
|
|
self.results = []
|
2019-06-12 22:43:20 -05:00
|
|
|
if field == "track":
|
2019-06-12 17:44:45 -05:00
|
|
|
data = search_response.tracks
|
|
|
|
elif field == "artist":
|
|
|
|
data = []
|
|
|
|
artist = search_response.artists[0].id
|
2019-08-16 03:05:37 -05:00
|
|
|
if config.app["services"]["tidal"]["include_albums"]:
|
|
|
|
albums = self.session.get_artist_albums(artist)
|
|
|
|
for album in albums:
|
|
|
|
tracks = self.session.get_album_tracks(album.id)
|
|
|
|
for track in tracks:
|
2020-07-08 13:12:33 -05:00
|
|
|
track.album = album
|
2019-08-16 03:05:37 -05:00
|
|
|
data.append(track)
|
|
|
|
if config.app["services"]["tidal"]["include_compilations"]:
|
|
|
|
compilations = self.session.get_artist_albums_other(artist)
|
|
|
|
for album in compilations:
|
|
|
|
tracks = self.session.get_album_tracks(album.id)
|
|
|
|
for track in tracks:
|
|
|
|
data.append(track)
|
|
|
|
if config.app["services"]["tidal"]["include_singles"]:
|
|
|
|
singles = self.session.get_artist_albums_ep_singles(artist)
|
|
|
|
for album in singles:
|
|
|
|
tracks = self.session.get_album_tracks(album.id)
|
|
|
|
for track in tracks:
|
2020-07-08 13:12:33 -05:00
|
|
|
# track.single = True
|
2019-08-16 03:05:37 -05:00
|
|
|
data.append(track)
|
2019-06-12 17:44:45 -05:00
|
|
|
for search_result in data:
|
2019-06-18 16:16:29 -05:00
|
|
|
s = base.song(self)
|
2020-07-08 13:12:33 -05:00
|
|
|
s.title = search_result.name
|
2019-06-12 17:44:45 -05:00
|
|
|
s.artist = search_result.artist.name
|
|
|
|
s.duration = seconds_to_string(search_result.duration)
|
|
|
|
s.url = search_result.id
|
2020-07-08 13:12:33 -05:00
|
|
|
s.tracknumber = str(search_result.track_num)
|
|
|
|
s.album = search_result.album.name
|
|
|
|
print(search_result.album.num_tracks, search_result.album.name)
|
|
|
|
if search_result.album.num_tracks == None:
|
|
|
|
s.single = True
|
2019-06-13 12:33:00 -05:00
|
|
|
s.info = search_result
|
2020-07-08 13:12:33 -05:00
|
|
|
print(s.info)
|
2019-06-12 17:44:45 -05:00
|
|
|
self.results.append(s)
|
|
|
|
log.debug("{0} results found.".format(len(self.results)))
|
|
|
|
|
2019-10-06 11:26:09 -05:00
|
|
|
def format_number(self, number):
|
|
|
|
if number < 10:
|
|
|
|
return "0%d" % (number)
|
|
|
|
else:
|
|
|
|
return number
|
|
|
|
|
2019-06-12 17:44:45 -05:00
|
|
|
def get_download_url(self, url):
|
|
|
|
url = self.session.get_media_url(url)
|
|
|
|
if url.startswith("https://") or url.startswith("http://") == False:
|
|
|
|
url = "rtmp://"+url
|
|
|
|
return url
|
|
|
|
|
|
|
|
def format_track(self, item):
|
2020-07-08 13:12:33 -05:00
|
|
|
if not hasattr(item, "single"):
|
|
|
|
return "{0}. {1}".format(self.format_number(item.tracknumber), item.title)
|
|
|
|
else:
|
|
|
|
return "{title}. {artist}. {duration}".format(title=item.title, duration=item.duration, artist=item.artist)
|
2019-06-21 14:47:24 -05:00
|
|
|
|
|
|
|
class settings(base.baseSettings):
|
|
|
|
name = _("Tidal")
|
|
|
|
config_section = "tidal"
|
|
|
|
|
|
|
|
def get_quality_list(self):
|
|
|
|
results = dict(low=_("Low"), high=_("High"), lossless=_("Lossless"))
|
|
|
|
return results
|
|
|
|
|
|
|
|
def get_quality_value(self, *args, **kwargs):
|
|
|
|
q = self.get_quality_list()
|
|
|
|
for i in q.keys():
|
|
|
|
if q.get(i) == self.quality.GetStringSelection():
|
|
|
|
return i
|
|
|
|
|
2019-06-24 13:27:42 -05:00
|
|
|
def set_quality_value(self, value, *args, **kwargs):
|
|
|
|
q = self.get_quality_list()
|
|
|
|
for i in q.keys():
|
|
|
|
if i == value:
|
|
|
|
self.quality.SetStringSelection(q.get(i))
|
|
|
|
break
|
|
|
|
|
2019-06-21 14:47:24 -05:00
|
|
|
def __init__(self, parent):
|
|
|
|
super(settings, self).__init__(parent=parent)
|
|
|
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
self.enabled = wx.CheckBox(self, wx.NewId(), _("Enable this service"))
|
|
|
|
self.enabled.Bind(wx.EVT_CHECKBOX, self.on_enabled)
|
|
|
|
self.map.append(("enabled", self.enabled))
|
|
|
|
sizer.Add(self.enabled, 0, wx.ALL, 5)
|
|
|
|
username = wx.StaticText(self, wx.NewId(), _("Tidal username or email address"))
|
|
|
|
self.username = wx.TextCtrl(self, wx.NewId())
|
|
|
|
usernamebox = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
usernamebox.Add(username, 0, wx.ALL, 5)
|
|
|
|
usernamebox.Add(self.username, 0, wx.ALL, 5)
|
|
|
|
sizer.Add(usernamebox, 0, wx.ALL, 5)
|
|
|
|
self.map.append(("username", self.username))
|
|
|
|
password = wx.StaticText(self, wx.NewId(), _("Password"))
|
|
|
|
self.password = wx.TextCtrl(self, wx.NewId(), style=wx.TE_PASSWORD)
|
|
|
|
passwordbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
passwordbox.Add(password, 0, wx.ALL, 5)
|
|
|
|
passwordbox.Add(self.password, 0, wx.ALL, 5)
|
|
|
|
sizer.Add(passwordbox, 0, wx.ALL, 5)
|
|
|
|
self.map.append(("password", self.password))
|
|
|
|
self.get_account = wx.Button(self, wx.NewId(), _("You can subscribe for a tidal account here"))
|
|
|
|
self.get_account.Bind(wx.EVT_BUTTON, self.on_get_account)
|
|
|
|
sizer.Add(self.get_account, 0, wx.ALL, 5)
|
|
|
|
quality = wx.StaticText(self, wx.NewId(), _("Audio quality"))
|
|
|
|
self.quality = wx.ComboBox(self, wx.NewId(), choices=[i for i in self.get_quality_list().values()], value=_("High"), style=wx.CB_READONLY)
|
|
|
|
qualitybox = wx.BoxSizer(wx.HORIZONTAL)
|
|
|
|
qualitybox.Add(quality, 0, wx.ALL, 5)
|
|
|
|
qualitybox.Add(self.quality, 0, wx.ALL, 5)
|
|
|
|
sizer.Add(qualitybox, 0, wx.ALL, 5)
|
|
|
|
# Monkeypatch for getting the right quality value here.
|
|
|
|
self.quality.GetValue = self.get_quality_value
|
2019-06-24 13:27:42 -05:00
|
|
|
self.quality.SetValue = self.set_quality_value
|
2019-06-21 14:47:24 -05:00
|
|
|
self.map.append(("quality", self.quality))
|
2019-08-16 03:05:37 -05:00
|
|
|
include = wx.StaticBoxSizer(parent=self, orient=wx.HORIZONTAL, label=_("Search by artist"))
|
|
|
|
self.include_albums = wx.CheckBox(include.GetStaticBox(), wx.NewId(), _("Include albums"))
|
|
|
|
self.include_compilations = wx.CheckBox(include.GetStaticBox(), wx.NewId(), _("Include compilations"))
|
|
|
|
self.include_singles = wx.CheckBox(include.GetStaticBox(), wx.NewId(), _("Include singles"))
|
|
|
|
sizer.Add(include, 0, wx.ALL, 5)
|
|
|
|
self.map.append(("include_albums", self.include_albums))
|
|
|
|
self.map.append(("include_compilations", self.include_compilations))
|
|
|
|
self.map.append(("include_singles", self.include_singles))
|
2019-06-21 14:47:24 -05:00
|
|
|
self.SetSizer(sizer)
|
|
|
|
|
|
|
|
def on_enabled(self, *args, **kwargs):
|
|
|
|
for i in self.map:
|
|
|
|
if i[1] != self.enabled:
|
|
|
|
if self.enabled.GetValue() == True:
|
|
|
|
i[1].Enable(True)
|
|
|
|
else:
|
|
|
|
i[1].Enable(False)
|
|
|
|
|
|
|
|
def on_get_account(self, *args, **kwargs):
|
|
|
|
webbrowser.open_new_tab("https://tidal.com")
|