wip
This commit is contained in:
parent
0bcd321086
commit
45dc0fd3ca
|
@ -0,0 +1,14 @@
|
||||||
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TidalEvent(Event):
|
||||||
|
"""Base class for Tidal events"""
|
||||||
|
|
||||||
|
|
||||||
|
class TidalPlaylistUpdatedEvent(TidalEvent):
|
||||||
|
"""
|
||||||
|
Event fired when a Tidal playlist is updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, playlist_id: str, *args, **kwargs):
|
||||||
|
super().__init__(*args, playlist_id=playlist_id, **kwargs)
|
|
@ -1,17 +1,31 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.message.response import Response
|
from platypush.context import Variable, get_bus
|
||||||
|
from platypush.message.event.music.tidal import TidalPlaylistUpdatedEvent
|
||||||
from platypush.plugins import RunnablePlugin, action
|
from platypush.plugins import RunnablePlugin, action
|
||||||
from platypush.plugins.media import PlayerState
|
|
||||||
|
|
||||||
|
|
||||||
class MusicTidalPlugin(RunnablePlugin):
|
class MusicTidalPlugin(RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
Plugin to interact with the user's Tidal account and library.
|
Plugin to interact with the user's Tidal account and library.
|
||||||
|
|
||||||
|
Upon the first login, the application will prompt you with a link to
|
||||||
|
connect to your Tidal account. Once authorized, you should no longer be
|
||||||
|
required to explicitly login.
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
|
||||||
|
* :class:`platypush.message.event.music.TidalPlaylistUpdatedEvent`: when a user playlist
|
||||||
|
is updated.
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **tidalapi** (``pip install tidalapi``)
|
* **tidalapi** (``pip install tidalapi``)
|
||||||
|
@ -20,15 +34,14 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
|
|
||||||
_base_url = 'https://api.tidalhifi.com/v1/'
|
_base_url = 'https://api.tidalhifi.com/v1/'
|
||||||
_default_credentials_file = os.path.join(
|
_default_credentials_file = os.path.join(
|
||||||
str(Config.get('workdir')),
|
str(Config.get('workdir')), 'tidal', 'credentials.json'
|
||||||
'tidal', 'credentials.json'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
quality: str = 'high',
|
quality: str = 'high',
|
||||||
credentials_file: str = _default_credentials_file,
|
credentials_file: str = _default_credentials_file,
|
||||||
**kwargs
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param quality: Default audio quality. Default: ``high``.
|
:param quality: Default audio quality. Default: ``high``.
|
||||||
|
@ -39,8 +52,9 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
"""
|
"""
|
||||||
from tidalapi import Quality
|
from tidalapi import Quality
|
||||||
|
|
||||||
super().__init__(self, **kwargs)
|
super().__init__(**kwargs)
|
||||||
self._credentials_file = credentials_file
|
self._credentials_file = credentials_file
|
||||||
|
self._user_playlists = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._quality = getattr(Quality, quality.lower())
|
self._quality = getattr(Quality, quality.lower())
|
||||||
|
@ -53,19 +67,23 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
def _oauth_open_saved_session(self):
|
def _oauth_open_saved_session(self):
|
||||||
|
if not self._session:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self._credentials_file, 'r') as f:
|
with open(self._credentials_file, 'r') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
self._session.load_oauth_session(
|
self._session.load_oauth_session(
|
||||||
data['token_type'],
|
data['token_type'], data['access_token'], data['refresh_token']
|
||||||
data['access_token'],
|
|
||||||
data['refresh_token']
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning('Could not load %s: %s', oauth_file, e)
|
self.logger.warning('Could not load %s: %s', self._credentials_file, e)
|
||||||
|
|
||||||
def _oauth_create_new_session(self):
|
def _oauth_create_new_session(self):
|
||||||
self._session.login_oauth_simple(function=self.logger.warning)
|
if not self._session:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._session.login_oauth_simple(function=self.logger.warning) # type: ignore
|
||||||
if self._session.check_login():
|
if self._session.check_login():
|
||||||
data = {
|
data = {
|
||||||
'token_type': self._session.token_type,
|
'token_type': self._session.token_type,
|
||||||
|
@ -74,18 +92,22 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
'refresh_token': self._session.refresh_token,
|
'refresh_token': self._session.refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(oauth_file, 'w') as outfile:
|
pathlib.Path(os.path.dirname(self._credentials_file)).mkdir(
|
||||||
|
parents=True, exist_ok=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(self._credentials_file, 'w') as outfile:
|
||||||
json.dump(data, outfile)
|
json.dump(data, outfile)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
from tidalapi import Config, Session
|
from tidalapi import Config, Session
|
||||||
|
|
||||||
if self._session and self._session.check_login():
|
if self._session and self._session.check_login():
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
# Attempt to reload the existing session from file
|
# Attempt to reload the existing session from file
|
||||||
# TODO populate Config object
|
self._session = Session(config=Config(quality=self._quality))
|
||||||
self._session = Session(config=Config())
|
|
||||||
self._oauth_open_saved_session()
|
self._oauth_open_saved_session()
|
||||||
if not self._session.check_login():
|
if not self._session.check_login():
|
||||||
# Create a new session if we couldn't load an existing one
|
# Create a new session if we couldn't load an existing one
|
||||||
|
@ -96,18 +118,19 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
|
|
||||||
def _api_request(self, url, *args, method='get', **kwargs):
|
def _api_request(self, url, *args, method='get', **kwargs):
|
||||||
method = getattr(requests, method.lower())
|
method = getattr(requests, method.lower())
|
||||||
url = urljoin(base_url, url)
|
url = urljoin(self._base_url, url)
|
||||||
kwargs['headers'] = kwargs.get('headers', {})
|
kwargs['headers'] = kwargs.get('headers', {})
|
||||||
kwargs['params'] = kwargs.get('params', {})
|
kwargs['params'] = kwargs.get('params', {})
|
||||||
kwargs['params'].update({
|
kwargs['params'].update(
|
||||||
'sessionId': self.session.session_id,
|
{
|
||||||
'countryCode': self.session.country_code,
|
'sessionId': self.session.session_id,
|
||||||
})
|
'countryCode': self.session.country_code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
rs = None
|
rs = None
|
||||||
kwargs['headers']['Authorization'] = '{type} {token}'.format(
|
kwargs['headers']['Authorization'] = '{type} {token}'.format(
|
||||||
type=self.session.token_type,
|
type=self.session.token_type, token=self.session.access_token
|
||||||
token=self.session.access_token
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -119,3 +142,66 @@ class MusicTidalPlugin(RunnablePlugin):
|
||||||
self.logger.error(rs.text)
|
self.logger.error(rs.text)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@action
|
||||||
|
def create_playlist(self, name: str, description: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Create a new playlist.
|
||||||
|
|
||||||
|
:param name: Playlist name.
|
||||||
|
:param description: Optional playlist description.
|
||||||
|
"""
|
||||||
|
return self._api_request(
|
||||||
|
url=f'users/{self.session.user.id}/playlists',
|
||||||
|
method='post',
|
||||||
|
data={
|
||||||
|
'title': name,
|
||||||
|
'description': description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def delete_playlist(self, playlist_id: str):
|
||||||
|
"""
|
||||||
|
Delete a playlist by ID.
|
||||||
|
|
||||||
|
:param playlist_id: ID of the playlist to delete.
|
||||||
|
"""
|
||||||
|
self._api_request(url=f'playlists/{playlist_id}', method='delete')
|
||||||
|
|
||||||
|
@action
|
||||||
|
def add_to_playlist(self, playlist_id: str, track_ids: Iterable[str]):
|
||||||
|
"""
|
||||||
|
Append one or more tracks to a playlist.
|
||||||
|
|
||||||
|
:param playlist_id: Target playlist ID.
|
||||||
|
:param track_ids: List of track IDs to append.
|
||||||
|
"""
|
||||||
|
return self._api_request(
|
||||||
|
url=f'playlists/{playlist_id}/items',
|
||||||
|
method='post',
|
||||||
|
headers={
|
||||||
|
'If-None-Match': None,
|
||||||
|
},
|
||||||
|
data={
|
||||||
|
'onArtifactNotFound': 'SKIP',
|
||||||
|
'onDupes': 'SKIP',
|
||||||
|
'trackIds': ','.join(map(str, track_ids)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
while not self.should_stop():
|
||||||
|
playlists = self.session.user.playlists() # type: ignore
|
||||||
|
|
||||||
|
for pl in playlists:
|
||||||
|
last_updated_var = Variable(f'TIDAL_PLAYLIST_LAST_UPDATE[{pl.id}]')
|
||||||
|
prev_last_updated = last_updated_var.get()
|
||||||
|
if prev_last_updated:
|
||||||
|
prev_last_updated = datetime.fromisoformat(prev_last_updated)
|
||||||
|
if pl.last_updated > prev_last_updated:
|
||||||
|
get_bus().post(TidalPlaylistUpdatedEvent(playlist_id=pl.id))
|
||||||
|
|
||||||
|
if not prev_last_updated or pl.last_updated > prev_last_updated:
|
||||||
|
last_updated_var.set(pl.last_updated.isoformat())
|
||||||
|
|
||||||
|
self.wait_stop(self.poll_interval)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
manifest:
|
manifest:
|
||||||
events: {}
|
events:
|
||||||
|
- platypush.message.event.music.TidalPlaylistUpdatedEvent: when a user playlist
|
||||||
|
is updated.
|
||||||
install:
|
install:
|
||||||
pip:
|
pip:
|
||||||
- tidalapi
|
- tidalapi
|
||||||
|
|
Loading…
Reference in New Issue