migrated to uri id base
This commit is contained in:
parent
6076ecd610
commit
2bd26df92f
4
alarm.py
4
alarm.py
@ -2,6 +2,7 @@ from spotframework.net.user import NetworkUser
|
|||||||
from spotframework.net.network import Network
|
from spotframework.net.network import Network
|
||||||
import spotframework.net.const as const
|
import spotframework.net.const as const
|
||||||
import spotframework.io.json as json
|
import spotframework.io.json as json
|
||||||
|
import spotframework.util.monthstrings as month
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
@ -62,7 +63,8 @@ if __name__ == '__main__':
|
|||||||
playlists = network.get_user_playlists()
|
playlists = network.get_user_playlists()
|
||||||
|
|
||||||
if data['alarm']['use_month']:
|
if data['alarm']['use_month']:
|
||||||
playlisturi = next((i.uri for i in playlists if i.name == date.strftime("%B %-y").lower()), data['alarm']['uri'])
|
playlisturi = next((i.uri for i in playlists if i.name == month.get_this_month()),
|
||||||
|
data['alarm']['uri'])
|
||||||
else:
|
else:
|
||||||
playlisturi = data['alarm']['uri']
|
playlisturi = data['alarm']['uri']
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ if __name__ == '__main__':
|
|||||||
playlists = network.get_user_playlists()
|
playlists = network.get_user_playlists()
|
||||||
|
|
||||||
for playlist in playlists:
|
for playlist in playlists:
|
||||||
playlist.tracks = network.get_playlist_tracks(playlist.playlist_id)
|
playlist.tracks = network.get_playlist_tracks(playlist.uri)
|
||||||
|
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from spotframework.engine.processor.added import AddedSince
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from spotframework.model.track import SpotifyTrack
|
from spotframework.model.track import SpotifyTrack
|
||||||
from spotframework.model.playlist import SpotifyPlaylist
|
from spotframework.model.playlist import SpotifyPlaylist
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from spotframework.net.network import Network
|
from spotframework.net.network import Network
|
||||||
from spotframework.engine.processor.abstract import AbstractProcessor
|
from spotframework.engine.processor.abstract import AbstractProcessor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -44,7 +45,7 @@ class PlaylistEngine:
|
|||||||
playlist: SpotifyPlaylist) -> None:
|
playlist: SpotifyPlaylist) -> None:
|
||||||
logger.info(f"pulling tracks for {playlist.name}")
|
logger.info(f"pulling tracks for {playlist.name}")
|
||||||
|
|
||||||
tracks = self.net.get_playlist_tracks(playlist.playlist_id)
|
tracks = self.net.get_playlist_tracks(playlist.uri)
|
||||||
if tracks and len(tracks) > 0:
|
if tracks and len(tracks) > 0:
|
||||||
playlist.tracks = tracks
|
playlist.tracks = tracks
|
||||||
else:
|
else:
|
||||||
@ -94,7 +95,7 @@ class PlaylistEngine:
|
|||||||
tracks = processor.process(tracks)
|
tracks = processor.process(tracks)
|
||||||
|
|
||||||
if include_recommendations:
|
if include_recommendations:
|
||||||
recommendations = self.net.get_recommendations(tracks=[i.spotify_id for i in tracks],
|
recommendations = self.net.get_recommendations(tracks=[i.uri.object_id for i in tracks],
|
||||||
response_limit=recommendation_limit)
|
response_limit=recommendation_limit)
|
||||||
if recommendations and len(recommendations) > 0:
|
if recommendations and len(recommendations) > 0:
|
||||||
tracks += recommendations
|
tracks += recommendations
|
||||||
@ -137,16 +138,16 @@ class PlaylistEngine:
|
|||||||
|
|
||||||
def reorder_playlist_by_added_date(self,
|
def reorder_playlist_by_added_date(self,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
playlistid: str = None,
|
uri: Uri = None,
|
||||||
reverse: bool = False):
|
reverse: bool = False):
|
||||||
if name is None and playlistid is None:
|
if name is None and uri is None:
|
||||||
logger.error('no playlist name or id provided')
|
logger.error('no playlist name or id provided')
|
||||||
raise ValueError('no playlist name or id provided')
|
raise ValueError('no playlist name or id provided')
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
playlist = next((i for i in self.playlists if i.name == name), None)
|
playlist = next((i for i in self.playlists if i.name == name), None)
|
||||||
else:
|
else:
|
||||||
playlist = next((i for i in self.playlists if i.spotify_id == playlistid), None)
|
playlist = next((i for i in self.playlists if i.uri == uri), None)
|
||||||
|
|
||||||
if playlist is None:
|
if playlist is None:
|
||||||
logger.error('playlist not found')
|
logger.error('playlist not found')
|
||||||
@ -171,9 +172,9 @@ class PlaylistEngine:
|
|||||||
|
|
||||||
def execute_playlist(self,
|
def execute_playlist(self,
|
||||||
tracks: List[SpotifyTrack],
|
tracks: List[SpotifyTrack],
|
||||||
playlist_id: str) -> Optional[Response]:
|
uri: Uri) -> Optional[Response]:
|
||||||
|
|
||||||
resp = self.net.replace_playlist_tracks(playlist_id, [i.uri for i in tracks])
|
resp = self.net.replace_playlist_tracks(uri, [i.uri for i in tracks])
|
||||||
if resp:
|
if resp:
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
@ -182,7 +183,7 @@ class PlaylistEngine:
|
|||||||
|
|
||||||
def change_description(self,
|
def change_description(self,
|
||||||
playlistparts: List[str],
|
playlistparts: List[str],
|
||||||
playlist_id: str,
|
uri: Uri,
|
||||||
overwrite: bool = None,
|
overwrite: bool = None,
|
||||||
suffix: str = None) -> Optional[Response]:
|
suffix: str = None) -> Optional[Response]:
|
||||||
|
|
||||||
@ -194,7 +195,7 @@ class PlaylistEngine:
|
|||||||
if suffix:
|
if suffix:
|
||||||
string += f' - {str(suffix)}'
|
string += f' - {str(suffix)}'
|
||||||
|
|
||||||
resp = self.net.change_playlist_details(playlist_id, description=string)
|
resp = self.net.change_playlist_details(uri, description=string)
|
||||||
if resp:
|
if resp:
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
from spotframework.util.console import Color
|
from spotframework.util.console import Color
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from spotframework.model.artist import Artist
|
from spotframework.model.artist import Artist
|
||||||
|
|
||||||
@ -35,8 +36,7 @@ class SpotifyAlbum(Album):
|
|||||||
artists: List[Artist],
|
artists: List[Artist],
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
spotify_id: str = None,
|
uri: Union[str, Uri] = None,
|
||||||
uri: str = None,
|
|
||||||
|
|
||||||
genres: List[str] = None,
|
genres: List[str] = None,
|
||||||
tracks: List = None,
|
tracks: List = None,
|
||||||
@ -50,7 +50,9 @@ class SpotifyAlbum(Album):
|
|||||||
super().__init__(name, artists)
|
super().__init__(name, artists)
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
self.spotify_id = spotify_id
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
|
||||||
self.genres = genres
|
self.genres = genres
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import List
|
from typing import List, Union
|
||||||
from spotframework.util.console import Color
|
from spotframework.util.console import Color
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
|
|
||||||
|
|
||||||
class Artist:
|
class Artist:
|
||||||
@ -19,8 +20,7 @@ class SpotifyArtist(Artist):
|
|||||||
name: str,
|
name: str,
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
spotify_id: str = None,
|
uri: Union[str, Uri] = None,
|
||||||
uri: str = None,
|
|
||||||
|
|
||||||
genres: List[str] = None,
|
genres: List[str] = None,
|
||||||
|
|
||||||
@ -29,7 +29,9 @@ class SpotifyArtist(Artist):
|
|||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
self.spotify_id = spotify_id
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
|
||||||
self.genres = genres
|
self.genres = genres
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from spotframework.model.user import User
|
from spotframework.model.user import User
|
||||||
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack
|
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from spotframework.util.console import Color
|
from spotframework.util.console import Color
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -119,14 +120,13 @@ class Playlist:
|
|||||||
class SpotifyPlaylist(Playlist):
|
class SpotifyPlaylist(Playlist):
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
playlistid: str,
|
uri: Union[str, Uri],
|
||||||
|
|
||||||
name: str = None,
|
name: str = None,
|
||||||
owner: User = None,
|
owner: User = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
uri: str = None,
|
|
||||||
|
|
||||||
collaborative: bool = None,
|
collaborative: bool = None,
|
||||||
public: bool = None,
|
public: bool = None,
|
||||||
@ -134,10 +134,12 @@ class SpotifyPlaylist(Playlist):
|
|||||||
|
|
||||||
super().__init__(name=name, description=description)
|
super().__init__(name=name, description=description)
|
||||||
|
|
||||||
self.playlist_id = playlistid
|
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
|
||||||
self.collaborative = collaborative
|
self.collaborative = collaborative
|
||||||
@ -147,7 +149,6 @@ class SpotifyPlaylist(Playlist):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
||||||
prefix = f'\n==={self.name}===\n\n' if self.name is not None else ''
|
prefix = f'\n==={self.name}===\n\n' if self.name is not None else ''
|
||||||
prefix += f'id: {self.playlist_id}\n' if self.playlist_id is not None else ''
|
|
||||||
prefix += f'uri: {self.uri}\n' if self.uri is not None else ''
|
prefix += f'uri: {self.uri}\n' if self.uri is not None else ''
|
||||||
|
|
||||||
table = prefix + self.get_tracks_string() + '\n' + f'total: {len(self)}'
|
table = prefix + self.get_tracks_string() + '\n' + f'total: {len(self)}'
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from spotframework.model.track import Track
|
from spotframework.model.track import Track
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
uri: str,
|
uri: Union[str, Uri],
|
||||||
object_type: str = None,
|
object_type: str = None,
|
||||||
href: str = None,
|
href: str = None,
|
||||||
external_spot: str = None):
|
external_spot: str = None):
|
||||||
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.object_type = object_type
|
self.object_type = object_type
|
||||||
self.href = href
|
self.href = href
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from spotframework.util.console import Color
|
from spotframework.util.console import Color
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from spotframework.model.album import Album
|
from spotframework.model.album import Album
|
||||||
from spotframework.model.artist import Artist
|
from spotframework.model.artist import Artist
|
||||||
from spotframework.model.user import User
|
from spotframework.model.user import User
|
||||||
|
from spotframework.model.service import Context
|
||||||
|
|
||||||
|
|
||||||
class Track:
|
class Track:
|
||||||
@ -57,8 +59,7 @@ class SpotifyTrack(Track):
|
|||||||
artists: List[Artist],
|
artists: List[Artist],
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
spotify_id: str = None,
|
uri: Union[str, Uri] = None,
|
||||||
uri: str = None,
|
|
||||||
|
|
||||||
disc_number: int = None,
|
disc_number: int = None,
|
||||||
duration_ms: int = None,
|
duration_ms: int = None,
|
||||||
@ -73,7 +74,9 @@ class SpotifyTrack(Track):
|
|||||||
excplicit=explicit)
|
excplicit=explicit)
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
self.spotify_id = spotify_id
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.is_playable = is_playable
|
self.is_playable = is_playable
|
||||||
|
|
||||||
@ -90,13 +93,12 @@ class PlaylistTrack(SpotifyTrack):
|
|||||||
album: Album,
|
album: Album,
|
||||||
artists: List[Artist],
|
artists: List[Artist],
|
||||||
|
|
||||||
added_at: str,
|
added_at: datetime,
|
||||||
added_by: User,
|
added_by: User,
|
||||||
is_local: bool,
|
is_local: bool,
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
spotify_id: str = None,
|
uri: Union[str, Uri] = None,
|
||||||
uri: str = None,
|
|
||||||
|
|
||||||
disc_number: int = None,
|
disc_number: int = None,
|
||||||
duration_ms: int = None,
|
duration_ms: int = None,
|
||||||
@ -107,7 +109,6 @@ class PlaylistTrack(SpotifyTrack):
|
|||||||
):
|
):
|
||||||
super().__init__(name=name, album=album, artists=artists,
|
super().__init__(name=name, album=album, artists=artists,
|
||||||
href=href,
|
href=href,
|
||||||
spotify_id=spotify_id,
|
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
|
||||||
disc_number=disc_number,
|
disc_number=disc_number,
|
||||||
@ -116,10 +117,46 @@ class PlaylistTrack(SpotifyTrack):
|
|||||||
is_playable=is_playable,
|
is_playable=is_playable,
|
||||||
popularity=popularity)
|
popularity=popularity)
|
||||||
|
|
||||||
self.added_at = datetime.fromisoformat(added_at.replace('T', ' ').replace('Z', ''))
|
self.added_at = added_at
|
||||||
self.added_by = added_by
|
self.added_by = added_by
|
||||||
self.is_local = is_local
|
self.is_local = is_local
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return Color.BOLD + Color.YELLOW + 'PlaylistTrack' + Color.END + \
|
return Color.BOLD + Color.YELLOW + 'PlaylistTrack' + Color.END + \
|
||||||
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.added_at}'
|
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.added_at}'
|
||||||
|
|
||||||
|
|
||||||
|
class PlayedTrack(SpotifyTrack):
|
||||||
|
def __init__(self,
|
||||||
|
name: str,
|
||||||
|
album: Album,
|
||||||
|
artists: List[Artist],
|
||||||
|
|
||||||
|
href: str = None,
|
||||||
|
uri: Union[str, Uri] = None,
|
||||||
|
|
||||||
|
disc_number: int = None,
|
||||||
|
duration_ms: int = None,
|
||||||
|
explicit: bool = None,
|
||||||
|
is_playable: bool = None,
|
||||||
|
|
||||||
|
popularity: int = None,
|
||||||
|
|
||||||
|
played_at: datetime = None,
|
||||||
|
context: Context = None
|
||||||
|
):
|
||||||
|
super().__init__(name=name, album=album, artists=artists,
|
||||||
|
href=href,
|
||||||
|
uri=uri,
|
||||||
|
|
||||||
|
disc_number=disc_number,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
explicit=explicit,
|
||||||
|
is_playable=is_playable,
|
||||||
|
popularity=popularity)
|
||||||
|
self.played_at = played_at
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return Color.BOLD + Color.YELLOW + 'PlayedTrack' + Color.END + \
|
||||||
|
f': {self.name}, ({self.album}), {self.artists}, {self.uri}, {self.played_at}'
|
||||||
|
52
spotframework/model/uri.py
Normal file
52
spotframework/model/uri.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Uri:
|
||||||
|
|
||||||
|
class ObjectType(Enum):
|
||||||
|
track = 1
|
||||||
|
album = 2
|
||||||
|
artist = 3
|
||||||
|
user = 4
|
||||||
|
playlist = 5
|
||||||
|
|
||||||
|
def __init__(self, input_string: str):
|
||||||
|
self.object_type = None
|
||||||
|
self.object_id = None
|
||||||
|
self.username = None
|
||||||
|
|
||||||
|
parts = input_string.split(':')
|
||||||
|
|
||||||
|
if parts[0] != 'spotify':
|
||||||
|
raise ValueError('malformed uri')
|
||||||
|
|
||||||
|
if len(parts) == 3:
|
||||||
|
self.object_type = self.ObjectType[parts[1]]
|
||||||
|
self.object_id = parts[2]
|
||||||
|
elif len(parts) == 5:
|
||||||
|
if parts[1] != 'user':
|
||||||
|
raise ValueError('malformed uri')
|
||||||
|
self.object_type = self.ObjectType[parts[3]]
|
||||||
|
self.object_id = parts[4]
|
||||||
|
else:
|
||||||
|
raise ValueError(f'malformed uri: {len(parts)} parts')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.username:
|
||||||
|
return f'spotify:user:{self.username}:{self.object_type.name}:{self.object_id}'
|
||||||
|
else:
|
||||||
|
return f'spotify:{self.object_type.name}:{self.object_id}'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.username:
|
||||||
|
return f'URI: {self.username} / {self.object_type.name} / {self.object_id}'
|
||||||
|
else:
|
||||||
|
return f'URI: {self.object_type.name} / {self.object_id}'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Uri):
|
||||||
|
if other.object_type == self.object_type and other.object_id == self.object_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
@ -1,4 +1,6 @@
|
|||||||
from spotframework.util.console import Color
|
from spotframework.util.console import Color
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
class User:
|
class User:
|
||||||
@ -6,13 +8,16 @@ class User:
|
|||||||
username: str,
|
username: str,
|
||||||
|
|
||||||
href: str = None,
|
href: str = None,
|
||||||
uri: str = None,
|
uri: Union[str, Uri] = None,
|
||||||
|
|
||||||
display_name: str = None,
|
display_name: str = None,
|
||||||
ext_spotify: str = None):
|
ext_spotify: str = None):
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
||||||
self.href = href
|
self.href = href
|
||||||
|
if isinstance(uri, str):
|
||||||
|
self.uri = Uri(uri)
|
||||||
|
else:
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
|
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
|
@ -3,12 +3,14 @@ import random
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
from . import const
|
from . import const
|
||||||
from spotframework.net.parse import parse
|
from spotframework.net.parse import parse
|
||||||
from spotframework.net.user import NetworkUser
|
from spotframework.net.user import NetworkUser
|
||||||
from spotframework.model.playlist import SpotifyPlaylist
|
from spotframework.model.playlist import SpotifyPlaylist
|
||||||
from spotframework.model.track import Track, PlaylistTrack
|
from spotframework.model.track import Track, PlaylistTrack, PlayedTrack
|
||||||
from spotframework.model.service import CurrentlyPlaying, Device
|
from spotframework.model.service import CurrentlyPlaying, Device
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from requests.models import Response
|
from requests.models import Response
|
||||||
|
|
||||||
limit = 50
|
limit = 50
|
||||||
@ -121,20 +123,20 @@ class Network:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_playlist(self, playlistid: str) -> Optional[SpotifyPlaylist]:
|
def get_playlist(self, uri: Uri) -> Optional[SpotifyPlaylist]:
|
||||||
|
|
||||||
logger.info(f"{playlistid}")
|
logger.info(f"{uri}")
|
||||||
|
|
||||||
tracks = self.get_playlist_tracks(playlistid)
|
tracks = self.get_playlist_tracks(uri)
|
||||||
|
|
||||||
if tracks is not None:
|
if tracks is not None:
|
||||||
|
|
||||||
playlist = SpotifyPlaylist(playlistid)
|
playlist = SpotifyPlaylist(uri.object_id)
|
||||||
playlist.tracks += tracks
|
playlist.tracks += tracks
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
else:
|
else:
|
||||||
logger.error(f"{playlistid} - no tracks returned")
|
logger.error(f"{uri} - no tracks returned")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create_playlist(self,
|
def create_playlist(self,
|
||||||
@ -142,7 +144,7 @@ class Network:
|
|||||||
name='New Playlist',
|
name='New Playlist',
|
||||||
public=True,
|
public=True,
|
||||||
collaborative=False,
|
collaborative=False,
|
||||||
description=None) -> Optional[dict]:
|
description=None) -> Optional[SpotifyPlaylist]:
|
||||||
|
|
||||||
json = {"name": name, "public": public, "collaborative": collaborative}
|
json = {"name": name, "public": public, "collaborative": collaborative}
|
||||||
|
|
||||||
@ -152,7 +154,7 @@ class Network:
|
|||||||
req = self._make_post_request('createPlaylist', f'users/{username}/playlists', json=json)
|
req = self._make_post_request('createPlaylist', f'users/{username}/playlists', json=json)
|
||||||
|
|
||||||
if 200 <= req.status_code < 300:
|
if 200 <= req.status_code < 300:
|
||||||
return req.json()
|
return parse.parse_playlist(req.json())
|
||||||
else:
|
else:
|
||||||
logger.error('error creating playlist')
|
logger.error('error creating playlist')
|
||||||
return None
|
return None
|
||||||
@ -195,28 +197,28 @@ class Network:
|
|||||||
logger.error('no playlists returned to filter')
|
logger.error('no playlists returned to filter')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_playlist_tracks(self, playlistid, offset=0) -> List[PlaylistTrack]:
|
def get_playlist_tracks(self, uri: Uri, offset=0) -> List[PlaylistTrack]:
|
||||||
|
|
||||||
logger.info(f"{playlistid}{' ' + str(offset) if offset is not 0 else ''}")
|
logger.info(f"{uri}{' ' + str(offset) if offset is not 0 else ''}")
|
||||||
|
|
||||||
tracks = []
|
tracks = []
|
||||||
|
|
||||||
params = {'offset': offset, 'limit': limit}
|
params = {'offset': offset, 'limit': limit}
|
||||||
|
|
||||||
resp = self._make_get_request('getPlaylistTracks', f'playlists/{playlistid}/tracks', params=params)
|
resp = self._make_get_request('getPlaylistTracks', f'playlists/{uri.object_id}/tracks', params=params)
|
||||||
|
|
||||||
if resp:
|
if resp:
|
||||||
if resp.get('items', None):
|
if resp.get('items', None):
|
||||||
tracks += [parse.parse_track(i) for i in resp.get('items', None)]
|
tracks += [parse.parse_track(i) for i in resp.get('items', None)]
|
||||||
else:
|
else:
|
||||||
logger.warning(f'{playlistid} no items returned')
|
logger.warning(f'{uri} no items returned')
|
||||||
|
|
||||||
if resp.get('next', None):
|
if resp.get('next', None):
|
||||||
more_tracks = self.get_playlist_tracks(playlistid, offset + limit)
|
more_tracks = self.get_playlist_tracks(uri, offset + limit)
|
||||||
if more_tracks:
|
if more_tracks:
|
||||||
tracks += more_tracks
|
tracks += more_tracks
|
||||||
else:
|
else:
|
||||||
logger.warning(f'{playlistid} error on response')
|
logger.warning(f'{uri} error on response')
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
@ -231,6 +233,32 @@ class Network:
|
|||||||
logger.error('no devices returned')
|
logger.error('no devices returned')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_recently_played_tracks(self,
|
||||||
|
response_limit: int = None,
|
||||||
|
after: datetime = None,
|
||||||
|
before: datetime = None) -> Optional[List[PlayedTrack]]:
|
||||||
|
|
||||||
|
logger.info("retrieving")
|
||||||
|
|
||||||
|
params = dict()
|
||||||
|
|
||||||
|
if response_limit:
|
||||||
|
params['limit'] = response_limit
|
||||||
|
if after and before:
|
||||||
|
raise ValueError('cant have before and after')
|
||||||
|
if after:
|
||||||
|
params['after'] = int(after.timestamp() * 1000)
|
||||||
|
if before:
|
||||||
|
params['before'] = int(before.timestamp() * 1000)
|
||||||
|
|
||||||
|
resp = self._make_get_request('getRecentlyPlayedTracks', 'me/player/recently-played', params=params)
|
||||||
|
|
||||||
|
if resp:
|
||||||
|
return [parse.parse_track(i) for i in resp['items']]
|
||||||
|
else:
|
||||||
|
logger.error('no tracks returned')
|
||||||
|
return None
|
||||||
|
|
||||||
def get_player(self) -> Optional[CurrentlyPlaying]:
|
def get_player(self) -> Optional[CurrentlyPlaying]:
|
||||||
|
|
||||||
logger.info("retrieved")
|
logger.info("retrieved")
|
||||||
@ -271,7 +299,7 @@ class Network:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def play(self, uri=None, uris=None, deviceid=None) -> Optional[Response]:
|
def play(self, uri: Uri = None, uris: List[Uri] = None, deviceid=None) -> Optional[Response]:
|
||||||
|
|
||||||
logger.info(f"{uri}{' ' + deviceid if deviceid is not None else ''}")
|
logger.info(f"{uri}{' ' + deviceid if deviceid is not None else ''}")
|
||||||
|
|
||||||
@ -286,9 +314,9 @@ class Network:
|
|||||||
payload = dict()
|
payload = dict()
|
||||||
|
|
||||||
if uri:
|
if uri:
|
||||||
payload['context_uri'] = uri
|
payload['context_uri'] = str(uri)
|
||||||
if uris:
|
if uris:
|
||||||
payload['uris'] = uris[:200]
|
payload['uris'] = [str(i) for i in uris[:200]]
|
||||||
|
|
||||||
req = self._make_put_request('play', 'me/player/play', params=params, json=payload)
|
req = self._make_put_request('play', 'me/player/play', params=params, json=payload)
|
||||||
if req:
|
if req:
|
||||||
@ -378,33 +406,34 @@ class Network:
|
|||||||
logger.error(f"{volume} not accepted value")
|
logger.error(f"{volume} not accepted value")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def replace_playlist_tracks(self, playlistid, uris):
|
def replace_playlist_tracks(self, uri: Uri, uris: List[Uri]):
|
||||||
|
|
||||||
logger.info(f"{playlistid}")
|
logger.info(f"{uri}")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
json = {"uris": uris[:100]}
|
json = {"uris": [str(i) for i in uris[:100]]}
|
||||||
|
|
||||||
req = self._make_put_request('replacePlaylistTracks', f'playlists/{playlistid}/tracks', json=json, headers=headers)
|
req = self._make_put_request('replacePlaylistTracks', f'playlists/{uri.object_id}/tracks',
|
||||||
|
json=json, headers=headers)
|
||||||
|
|
||||||
if req is not None:
|
if req is not None:
|
||||||
|
|
||||||
if len(uris) > 100:
|
if len(uris) > 100:
|
||||||
return self.add_playlist_tracks(playlistid, uris[100:])
|
return self.add_playlist_tracks(uri, uris[100:])
|
||||||
|
|
||||||
return req
|
return req
|
||||||
else:
|
else:
|
||||||
logger.error(f'error replacing playlist tracks, total: {len(uris)}')
|
logger.error(f'error replacing playlist tracks, total: {len(uris)}')
|
||||||
|
|
||||||
def change_playlist_details(self,
|
def change_playlist_details(self,
|
||||||
playlistid,
|
uri: Uri,
|
||||||
name=None,
|
name=None,
|
||||||
public=None,
|
public=None,
|
||||||
collaborative=None,
|
collaborative=None,
|
||||||
description=None) -> Optional[Response]:
|
description=None) -> Optional[Response]:
|
||||||
|
|
||||||
logger.info(f"{playlistid}")
|
logger.info(f"{uri}")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
@ -426,22 +455,24 @@ class Network:
|
|||||||
logger.warning('update dictionairy length 0')
|
logger.warning('update dictionairy length 0')
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
req = self._make_put_request('changePlaylistDetails', f'playlists/{playlistid}', json=json, headers=headers)
|
req = self._make_put_request('changePlaylistDetails', f'playlists/{uri.object_id}',
|
||||||
|
json=json, headers=headers)
|
||||||
if req:
|
if req:
|
||||||
return req
|
return req
|
||||||
else:
|
else:
|
||||||
logger.error('error updating details')
|
logger.error('error updating details')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def add_playlist_tracks(self, playlistid: str, uris: List[str]) -> List[dict]:
|
def add_playlist_tracks(self, uri: Uri, uris: List[Uri]) -> List[dict]:
|
||||||
|
|
||||||
logger.info(f"{playlistid}")
|
logger.info(f"{uri}")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
json = {"uris": uris[:100]}
|
json = {"uris": [str(i) for i in uris[:100]]}
|
||||||
|
|
||||||
req = self._make_post_request('addPlaylistTracks', f'playlists/{playlistid}/tracks', json=json, headers=headers)
|
req = self._make_post_request('addPlaylistTracks', f'playlists/{uri.object_id}/tracks',
|
||||||
|
json=json, headers=headers)
|
||||||
|
|
||||||
if req is not None:
|
if req is not None:
|
||||||
resp = req.json()
|
resp = req.json()
|
||||||
@ -450,12 +481,12 @@ class Network:
|
|||||||
|
|
||||||
if len(uris) > 100:
|
if len(uris) > 100:
|
||||||
|
|
||||||
snapshots += self.add_playlist_tracks(playlistid, uris[100:])
|
snapshots += self.add_playlist_tracks(uri, uris[100:])
|
||||||
|
|
||||||
return snapshots
|
return snapshots
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.error(f'error retrieving tracks {playlistid}, total: {len(uris)}')
|
logger.error(f'error retrieving tracks {uri}, total: {len(uris)}')
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_recommendations(self, tracks=None, artists=None, response_limit=10) -> Optional[List[Track]]:
|
def get_recommendations(self, tracks=None, artists=None, response_limit=10) -> Optional[List[Track]]:
|
||||||
@ -490,17 +521,17 @@ class Network:
|
|||||||
playlist: SpotifyPlaylist,
|
playlist: SpotifyPlaylist,
|
||||||
append_tracks: bool = False):
|
append_tracks: bool = False):
|
||||||
|
|
||||||
if playlist.playlist_id:
|
if playlist.uri:
|
||||||
if playlist.tracks == -1:
|
if playlist.tracks == -1:
|
||||||
self.replace_playlist_tracks(playlist.playlist_id, [])
|
self.replace_playlist_tracks(playlist.uri, [])
|
||||||
elif playlist.tracks:
|
elif playlist.tracks:
|
||||||
if append_tracks:
|
if append_tracks:
|
||||||
self.add_playlist_tracks(playlist.playlist_id, [i.uri for i in playlist.tracks])
|
self.add_playlist_tracks(playlist.uri, [i.uri for i in playlist.tracks])
|
||||||
else:
|
else:
|
||||||
self.replace_playlist_tracks(playlist.playlist_id, [i.uri for i in playlist.tracks])
|
self.replace_playlist_tracks(playlist.uri, [i.uri for i in playlist.tracks])
|
||||||
|
|
||||||
if playlist.name or playlist.collaborative or playlist.public or playlist.description:
|
if playlist.name or playlist.collaborative or playlist.public or playlist.description:
|
||||||
self.change_playlist_details(playlist.playlist_id,
|
self.change_playlist_details(playlist.uri,
|
||||||
playlist.name,
|
playlist.name,
|
||||||
playlist.public,
|
playlist.public,
|
||||||
playlist.collaborative,
|
playlist.collaborative,
|
||||||
@ -510,12 +541,12 @@ class Network:
|
|||||||
logger.error('playlist has no id')
|
logger.error('playlist has no id')
|
||||||
|
|
||||||
def reorder_playlist_tracks(self,
|
def reorder_playlist_tracks(self,
|
||||||
playlistid: str,
|
uri: Uri,
|
||||||
range_start: int,
|
range_start: int,
|
||||||
range_length: int,
|
range_length: int,
|
||||||
insert_before: int) -> Optional[Response]:
|
insert_before: int) -> Optional[Response]:
|
||||||
|
|
||||||
logger.info(f'id: {playlistid}')
|
logger.info(f'id: {uri}')
|
||||||
|
|
||||||
if range_start < 0:
|
if range_start < 0:
|
||||||
logger.error('range_start must be positive')
|
logger.error('range_start must be positive')
|
||||||
@ -531,7 +562,7 @@ class Network:
|
|||||||
'range_length': range_length,
|
'range_length': range_length,
|
||||||
'insert_before': insert_before}
|
'insert_before': insert_before}
|
||||||
|
|
||||||
resp = self._make_put_request('reorderPlaylistTracks', f'playlists/{playlistid}/tracks', json=json)
|
resp = self._make_put_request('reorderPlaylistTracks', f'playlists/{uri.object_id}/tracks', json=json)
|
||||||
|
|
||||||
if resp:
|
if resp:
|
||||||
return resp
|
return resp
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from spotframework.model.artist import SpotifyArtist
|
from spotframework.model.artist import SpotifyArtist
|
||||||
from spotframework.model.album import SpotifyAlbum
|
from spotframework.model.album import SpotifyAlbum
|
||||||
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack
|
from spotframework.model.track import Track, SpotifyTrack, PlaylistTrack, PlayedTrack
|
||||||
from spotframework.model.playlist import SpotifyPlaylist
|
from spotframework.model.playlist import SpotifyPlaylist
|
||||||
from spotframework.model.user import User
|
from spotframework.model.user import User
|
||||||
from spotframework.model.service import Context, CurrentlyPlaying, Device
|
from spotframework.model.service import Context, CurrentlyPlaying, Device
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
def parse_artist(artist_dict) -> SpotifyArtist:
|
def parse_artist(artist_dict) -> SpotifyArtist:
|
||||||
@ -12,7 +13,6 @@ def parse_artist(artist_dict) -> SpotifyArtist:
|
|||||||
name = artist_dict.get('name', None)
|
name = artist_dict.get('name', None)
|
||||||
|
|
||||||
href = artist_dict.get('href', None)
|
href = artist_dict.get('href', None)
|
||||||
spotify_id = artist_dict.get('id', None)
|
|
||||||
uri = artist_dict.get('uri', None)
|
uri = artist_dict.get('uri', None)
|
||||||
|
|
||||||
genres = artist_dict.get('genres', None)
|
genres = artist_dict.get('genres', None)
|
||||||
@ -23,7 +23,6 @@ def parse_artist(artist_dict) -> SpotifyArtist:
|
|||||||
|
|
||||||
return SpotifyArtist(name,
|
return SpotifyArtist(name,
|
||||||
href=href,
|
href=href,
|
||||||
spotify_id=spotify_id,
|
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
|
||||||
genres=genres,
|
genres=genres,
|
||||||
@ -39,7 +38,6 @@ def parse_album(album_dict) -> SpotifyAlbum:
|
|||||||
artists = [parse_artist(i) for i in album_dict.get('artists', [])]
|
artists = [parse_artist(i) for i in album_dict.get('artists', [])]
|
||||||
|
|
||||||
href = album_dict.get('href', None)
|
href = album_dict.get('href', None)
|
||||||
spotify_id = album_dict.get('id', None)
|
|
||||||
uri = album_dict.get('uri', None)
|
uri = album_dict.get('uri', None)
|
||||||
|
|
||||||
genres = album_dict.get('genres', None)
|
genres = album_dict.get('genres', None)
|
||||||
@ -55,7 +53,6 @@ def parse_album(album_dict) -> SpotifyAlbum:
|
|||||||
artists=artists,
|
artists=artists,
|
||||||
|
|
||||||
href=href,
|
href=href,
|
||||||
spotify_id=spotify_id,
|
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
|
||||||
genres=genres,
|
genres=genres,
|
||||||
@ -68,7 +65,7 @@ def parse_album(album_dict) -> SpotifyAlbum:
|
|||||||
popularity=popularity)
|
popularity=popularity)
|
||||||
|
|
||||||
|
|
||||||
def parse_track(track_dict) -> Track:
|
def parse_track(track_dict) -> Union[Track, SpotifyTrack, PlayedTrack]:
|
||||||
|
|
||||||
if 'track' in track_dict:
|
if 'track' in track_dict:
|
||||||
track = track_dict.get('track', None)
|
track = track_dict.get('track', None)
|
||||||
@ -84,12 +81,9 @@ def parse_track(track_dict) -> Track:
|
|||||||
else:
|
else:
|
||||||
album = None
|
album = None
|
||||||
|
|
||||||
# print(album.name)
|
|
||||||
|
|
||||||
artists = [parse_artist(i) for i in track.get('artists', [])]
|
artists = [parse_artist(i) for i in track.get('artists', [])]
|
||||||
|
|
||||||
href = track.get('href', None)
|
href = track.get('href', None)
|
||||||
spotify_id = track.get('id', None)
|
|
||||||
uri = track.get('uri', None)
|
uri = track.get('uri', None)
|
||||||
|
|
||||||
disc_number = track.get('disc_number', None)
|
disc_number = track.get('disc_number', None)
|
||||||
@ -101,9 +95,16 @@ def parse_track(track_dict) -> Track:
|
|||||||
|
|
||||||
added_by = parse_user(track_dict.get('added_by')) if track_dict.get('added_by', None) else None
|
added_by = parse_user(track_dict.get('added_by')) if track_dict.get('added_by', None) else None
|
||||||
added_at = track_dict.get('added_at', None)
|
added_at = track_dict.get('added_at', None)
|
||||||
|
if added_at:
|
||||||
|
added_at = datetime.datetime.strptime(added_at, '%Y-%m-%dT%H:%M:%S%z')
|
||||||
is_local = track_dict.get('is_local', None)
|
is_local = track_dict.get('is_local', None)
|
||||||
|
|
||||||
# print(album.name)
|
played_at = track_dict.get('played_at', None)
|
||||||
|
if played_at:
|
||||||
|
played_at = datetime.datetime.strptime(played_at, '%Y-%m-%dT%H:%M:%S.%f%z')
|
||||||
|
context = track_dict.get('context', None)
|
||||||
|
if context:
|
||||||
|
context = parse_context(context)
|
||||||
|
|
||||||
if added_at or added_by or is_local:
|
if added_at or added_by or is_local:
|
||||||
return PlaylistTrack(name=name,
|
return PlaylistTrack(name=name,
|
||||||
@ -115,7 +116,6 @@ def parse_track(track_dict) -> Track:
|
|||||||
is_local=is_local,
|
is_local=is_local,
|
||||||
|
|
||||||
href=href,
|
href=href,
|
||||||
spotify_id=spotify_id,
|
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
|
||||||
disc_number=disc_number,
|
disc_number=disc_number,
|
||||||
@ -124,13 +124,28 @@ def parse_track(track_dict) -> Track:
|
|||||||
is_playable=is_playable,
|
is_playable=is_playable,
|
||||||
|
|
||||||
popularity=popularity)
|
popularity=popularity)
|
||||||
|
elif played_at or context:
|
||||||
|
return PlayedTrack(name=name,
|
||||||
|
album=album,
|
||||||
|
artists=artists,
|
||||||
|
|
||||||
|
href=href,
|
||||||
|
uri=uri,
|
||||||
|
|
||||||
|
disc_number=disc_number,
|
||||||
|
duration_ms=duration_ms,
|
||||||
|
explicit=explicit,
|
||||||
|
is_playable=is_playable,
|
||||||
|
|
||||||
|
popularity=popularity,
|
||||||
|
played_at=played_at,
|
||||||
|
context=context)
|
||||||
else:
|
else:
|
||||||
return SpotifyTrack(name=name,
|
return SpotifyTrack(name=name,
|
||||||
album=album,
|
album=album,
|
||||||
artists=artists,
|
artists=artists,
|
||||||
|
|
||||||
href=href,
|
href=href,
|
||||||
spotify_id=spotify_id,
|
|
||||||
uri=uri,
|
uri=uri,
|
||||||
|
|
||||||
disc_number=disc_number,
|
disc_number=disc_number,
|
||||||
@ -164,7 +179,6 @@ def parse_playlist(playlist_dict) -> SpotifyPlaylist:
|
|||||||
ext_spotify = playlist_dict['external_urls']['spotify']
|
ext_spotify = playlist_dict['external_urls']['spotify']
|
||||||
|
|
||||||
href = playlist_dict.get('href', None)
|
href = playlist_dict.get('href', None)
|
||||||
playlist_id = playlist_dict.get('id', None)
|
|
||||||
description = playlist_dict.get('description', None)
|
description = playlist_dict.get('description', None)
|
||||||
|
|
||||||
name = playlist_dict.get('name', None)
|
name = playlist_dict.get('name', None)
|
||||||
@ -177,12 +191,11 @@ def parse_playlist(playlist_dict) -> SpotifyPlaylist:
|
|||||||
public = playlist_dict.get('public', None)
|
public = playlist_dict.get('public', None)
|
||||||
uri = playlist_dict.get('uri', None)
|
uri = playlist_dict.get('uri', None)
|
||||||
|
|
||||||
return SpotifyPlaylist(playlistid=playlist_id,
|
return SpotifyPlaylist(uri=uri,
|
||||||
name=name,
|
name=name,
|
||||||
owner=owner,
|
owner=owner,
|
||||||
description=description,
|
description=description,
|
||||||
href=href,
|
href=href,
|
||||||
uri=uri,
|
|
||||||
collaborative=collaborative,
|
collaborative=collaborative,
|
||||||
public=public,
|
public=public,
|
||||||
ext_spotify=ext_spotify)
|
ext_spotify=ext_spotify)
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from spotframework.net.network import Network
|
from spotframework.net.network import Network
|
||||||
from spotframework.model.track import SpotifyTrack
|
from spotframework.model.track import SpotifyTrack
|
||||||
|
from spotframework.model.album import SpotifyAlbum
|
||||||
|
from spotframework.model.playlist import SpotifyPlaylist
|
||||||
from spotframework.model.service import Context, Device
|
from spotframework.model.service import Context, Device
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ class Player:
|
|||||||
return self.last_status
|
return self.last_status
|
||||||
|
|
||||||
def play(self,
|
def play(self,
|
||||||
context: Context = None,
|
context: Union[Context, SpotifyAlbum, SpotifyPlaylist] = None,
|
||||||
tracks: List[SpotifyTrack] = None,
|
tracks: List[SpotifyTrack] = None,
|
||||||
device: Device = None):
|
device: Device = None):
|
||||||
if context and tracks:
|
if context and tracks:
|
||||||
|
Loading…
Reference in New Issue
Block a user