From d0491d8dd0cfffb32311af5ee355352735c45c3a Mon Sep 17 00:00:00 2001 From: Manuel Cortez Date: Wed, 12 Jun 2019 17:44:45 -0500 Subject: [PATCH] Added tidal as a service --- changes.md | 8 +++- requirements.txt | 3 +- src/app-configuration.defaults | 6 +++ src/controller/mainController.py | 2 +- src/extractors/__init__.py | 6 ++- src/extractors/tidal.py | 76 ++++++++++++++++++++++++++++++++ src/wxUI/mainWindow.py | 2 +- 7 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/extractors/tidal.py diff --git a/changes.md b/changes.md index 94c9709..1f66644 100644 --- a/changes.md +++ b/changes.md @@ -1,5 +1,11 @@ ## Changelog +## Version 0.6 + +* Added a new and experimental extractor for supporting tidal. + * Take into account that this extractor requires you to have a paid account on tidal. Depending in the account level, you will be able to play and download music in high quality or lossless audio. MusicDL will handle both, though at the current moment, only downloading of lossless audio is implemented. + * There is a new search mode supported in this service. You can retrieve all work for a certain artist by using the protocol artist://, plus the name of the artist you want to retrieve. For example, artist://The beatles will retrieve everything made by the beatles available in the service. The search results will be grouped by albums, compilations and singles, in this order. + ## Version 0.4 * Fixed an error when creating a directory located in %appdata%, when using MusicDL as an installed version. MusicDL should be able to work normally again. @@ -7,7 +13,7 @@ * MusicDL will no longer set volume at 50% when it starts. It will save the volume in a settings file, so it will remember volume settings across restarts. * Added an option in the help menu to report an issue. You can use this feature for sending reports of problems you have encountered while using the application. You will need to provide your email address, though it will not be public anywhere. Your email address will be used only for contacting you if necessary. * changes in Youtube module: - * Updated YoutubeDL to version 2018.10.05 + * Updated YoutubeDL to latest version. ## Version 0.3 diff --git a/requirements.txt b/requirements.txt index c27aadd..3683fae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ youtube-dl pyinstaller isodate configobj -winpaths \ No newline at end of file +winpaths +tidalapi \ No newline at end of file diff --git a/src/app-configuration.defaults b/src/app-configuration.defaults index 2dcbca4..b23304e 100644 --- a/src/app-configuration.defaults +++ b/src/app-configuration.defaults @@ -1,2 +1,8 @@ [main] volume = integer(default=50) + +[services] +[[tidal]] +username = string(default="") +password = string(default="") +quality=string(default="lossless") \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index 80795b9..c44b73c 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -96,7 +96,7 @@ class Controller(object): # Event functions. These functions will call other functions in a thread and are bound to widget events. def on_search(self, *args, **kwargs): - utils.call_threaded(self.search) + wx.CallAfter(self.search) def on_activated(self, *args, **kwargs): self.on_play() diff --git a/src/extractors/__init__.py b/src/extractors/__init__.py index 45ed0ed..aa5e87b 100644 --- a/src/extractors/__init__.py +++ b/src/extractors/__init__.py @@ -1,3 +1,7 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- -from . import mailru, youtube, zaycev \ No newline at end of file +import config +from . import mailru, youtube, zaycev +# conditional imports +if config.app["services"]["tidal"]["username"] != "" and config.app["services"]["tidal"]["password"] != "": + from . import tidal \ No newline at end of file diff --git a/src/extractors/tidal.py b/src/extractors/tidal.py new file mode 100644 index 0000000..3b9cfb5 --- /dev/null +++ b/src/extractors/tidal.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import logging +import tidalapi +import config +from .import baseFile +from update.utils import seconds_to_string +log = logging.getLogger("extractors.tidal.com") + +class interface(object): + name = "tidal" + + def __init__(self): + self.results = [] + self.needs_transcode = False + log.debug("started extraction service for {0}".format(self.name,)) + if hasattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"]): + quality = getattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"]) + else: + quality = tidalapi.Quality.high + _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) + + 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"] + 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 = [] + if field == "tracks": + data = search_response.tracks + elif field == "artist": + data = [] + artist = search_response.artists[0].id + albums = self.session.get_artist_albums(artist) + for album in albums: + tracks = self.session.get_album_tracks(album.id) + for track in tracks: + data.append(track) + 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) + 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: + data.append(track) + for search_result in data: + s = baseFile.song(self) + s.title = search_result.name + s.artist = search_result.artist.name + s.duration = seconds_to_string(search_result.duration) + s.url = search_result.id + self.results.append(s) + + log.debug("{0} results found.".format(len(self.results))) + + 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): + return "{title}. {artist}. {duration}".format(title=item.title, duration=item.duration, artist=item.artist) \ No newline at end of file diff --git a/src/wxUI/mainWindow.py b/src/wxUI/mainWindow.py index d2218b6..e234b1d 100644 --- a/src/wxUI/mainWindow.py +++ b/src/wxUI/mainWindow.py @@ -45,7 +45,7 @@ class mainWindow(wx.Frame): box.Add(lbl2, 0, wx.GROW) box.Add(self.text, 1, wx.GROW) box.Add(wx.StaticText(self.panel, wx.NewId(), _(u"Search in")), 0, wx.GROW) - self.extractor = wx.ComboBox(self.panel, wx.NewId(), choices=["youtube", "mail.ru", "zaycev.net"], value="youtube", style=wx.CB_READONLY) + self.extractor = wx.ComboBox(self.panel, wx.NewId(), choices=["youtube", "tidal", "mail.ru", "zaycev.net"], value="youtube", style=wx.CB_READONLY) box.Add(self.extractor, 1, wx.GROW) self.search = wx.Button(self.panel, wx.NewId(), _(u"Search")) self.search.SetDefault()