yutipy 2.1.1__py3-none-any.whl → 2.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of yutipy might be problematic. Click here for more details.
- yutipy/__init__.py +2 -0
- yutipy/cli/config.py +59 -23
- yutipy/exceptions.py +5 -1
- yutipy/lastfm.py +127 -0
- yutipy/spotify.py +38 -5
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/METADATA +2 -1
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/RECORD +11 -10
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/WHEEL +0 -0
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/entry_points.txt +0 -0
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/licenses/LICENSE +0 -0
- {yutipy-2.1.1.dist-info → yutipy-2.2.1.dist-info}/top_level.txt +0 -0
yutipy/__init__.py
CHANGED
yutipy/cli/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import webbrowser
|
|
3
|
+
|
|
3
4
|
from dotenv import load_dotenv, set_key
|
|
4
5
|
|
|
5
6
|
|
|
@@ -12,45 +13,80 @@ def run_config_wizard():
|
|
|
12
13
|
env_file = ".env"
|
|
13
14
|
load_dotenv(env_file)
|
|
14
15
|
|
|
15
|
-
# List of
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
# List of available services and their required environment variables
|
|
17
|
+
services = {
|
|
18
|
+
"Spotify": {
|
|
19
|
+
"SPOTIFY_CLIENT_ID": {
|
|
20
|
+
"description": "Spotify Client ID",
|
|
21
|
+
"url": "https://developer.spotify.com/dashboard",
|
|
22
|
+
"instructions": """
|
|
21
23
|
1. Go to your Spotify Developer Dashboard: https://developer.spotify.com/dashboard
|
|
22
24
|
2. Create a new app and fill in the required details.
|
|
23
25
|
3. Copy the "Client ID" and "Client Secret" from the app's settings.
|
|
24
26
|
4. Paste them here when prompted.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
""",
|
|
28
|
+
},
|
|
29
|
+
"SPOTIFY_CLIENT_SECRET": {
|
|
30
|
+
"description": "Spotify Client Secret",
|
|
31
|
+
"url": "https://developer.spotify.com/dashboard",
|
|
32
|
+
"instructions": "See the steps above for Spotify Client ID.",
|
|
33
|
+
},
|
|
31
34
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
"KKBox": {
|
|
36
|
+
"KKBOX_CLIENT_ID": {
|
|
37
|
+
"description": "KKBox Client ID",
|
|
38
|
+
"url": "https://developer.kkbox.com/",
|
|
39
|
+
"instructions": """
|
|
36
40
|
1. Go to the KKBOX Developer Portal: https://developer.kkbox.com/
|
|
37
41
|
2. Log in and create a new application.
|
|
38
42
|
3. Copy the "Client ID" and "Client Secret" from the app's settings.
|
|
39
43
|
4. Paste them here when prompted.
|
|
40
|
-
|
|
44
|
+
""",
|
|
45
|
+
},
|
|
46
|
+
"KKBOX_CLIENT_SECRET": {
|
|
47
|
+
"description": "KKBox Client Secret",
|
|
48
|
+
"url": "https://developer.kkbox.com/",
|
|
49
|
+
"instructions": "See the steps above for KKBox Client ID.",
|
|
50
|
+
},
|
|
41
51
|
},
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
"Last.fm": {
|
|
53
|
+
"LASTFM_API_KEY": {
|
|
54
|
+
"description": "Last.fm API Key",
|
|
55
|
+
"url": "https://www.last.fm/api/account/create",
|
|
56
|
+
"instructions": """
|
|
57
|
+
1. Go to the Last.fm API account creation page: https://www.last.fm/api/account/create
|
|
58
|
+
2. Log in with your Last.fm account.
|
|
59
|
+
3. Create a new application and fill in the required details.
|
|
60
|
+
4. Copy the "API Key" from the application settings.
|
|
61
|
+
5. Paste it here when prompted.
|
|
62
|
+
""",
|
|
63
|
+
},
|
|
46
64
|
},
|
|
47
65
|
}
|
|
48
66
|
|
|
67
|
+
# Display available services
|
|
68
|
+
print("Available services:")
|
|
69
|
+
for i, service in enumerate(services.keys(), start=1):
|
|
70
|
+
print(f"{i}. {service}")
|
|
71
|
+
|
|
72
|
+
# Prompt the user to select a service
|
|
73
|
+
choice = input("\nEnter the number of the service you want to configure: ").strip()
|
|
74
|
+
try:
|
|
75
|
+
service_name = list(services.keys())[int(choice) - 1]
|
|
76
|
+
except (IndexError, ValueError):
|
|
77
|
+
print("Invalid choice. Exiting configuration wizard.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
print(f"\nYou selected: {service_name}")
|
|
81
|
+
|
|
82
|
+
# Get the selected service's variables
|
|
83
|
+
selected_service = services[service_name]
|
|
84
|
+
|
|
49
85
|
# Track whether the browser has already been opened for a service
|
|
50
86
|
browser_opened = set()
|
|
51
87
|
|
|
52
|
-
# Prompt the user for each variable
|
|
53
|
-
for var, details in
|
|
88
|
+
# Prompt the user for each variable in the selected service
|
|
89
|
+
for var, details in selected_service.items():
|
|
54
90
|
current_value = os.getenv(var)
|
|
55
91
|
if current_value:
|
|
56
92
|
print(f"{details['description']} is already set.")
|
yutipy/exceptions.py
CHANGED
|
@@ -39,7 +39,11 @@ class ItunesException(YutipyException):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class KKBoxException(YutipyException):
|
|
42
|
-
"""Exception raised for
|
|
42
|
+
"""Exception raised for errors related to the KKBOX Open API."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LastFmException(YutipyException):
|
|
46
|
+
"""Exception raised for errors related to the LastFm API."""
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
class MusicYTException(YutipyException):
|
yutipy/lastfm.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
__all__ = ["LastFm", "LastFmException"]
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from pprint import pprint
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
from yutipy.exceptions import LastFmException
|
|
12
|
+
from yutipy.logger import logger
|
|
13
|
+
from yutipy.models import UserPlaying
|
|
14
|
+
from yutipy.utils.helpers import separate_artists
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
LASTFM_API_KEY = os.getenv("LASTFM_API_KEY")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LastFm:
|
|
22
|
+
"""
|
|
23
|
+
A class to interact with the Last.fm API for fetching user music data.
|
|
24
|
+
|
|
25
|
+
This class reads the ``LASTFM_API_KEY`` from environment variables or the ``.env`` file by default.
|
|
26
|
+
Alternatively, you can manually provide this values when creating an object.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, api_key: str = None):
|
|
30
|
+
"""
|
|
31
|
+
Initializes the LastFm class.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
lastfm_api_key (str, optional): The Last.fm API key. If not provided,
|
|
35
|
+
it will be fetched from the environment variable `LASTFM_API_KEY`.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
lastfm_api_key : str, optional
|
|
40
|
+
The Lastfm API Key (<https://www.last.fm/api>). Defaults to ``LASTFM_API_KEY`` from environment variable or the ``.env`` file.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
LastFmException: If the API key is not provided or found in the environment.
|
|
44
|
+
"""
|
|
45
|
+
self.api_key = api_key or LASTFM_API_KEY
|
|
46
|
+
|
|
47
|
+
if not self.api_key:
|
|
48
|
+
raise LastFmException(
|
|
49
|
+
"Lastfm API key was not found. Set it in environment variable or directly pass it when creating object."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._is_session_closed = False
|
|
53
|
+
|
|
54
|
+
self.__api_url = "https://ws.audioscrobbler.com/2.0"
|
|
55
|
+
self.__session = requests.Session()
|
|
56
|
+
|
|
57
|
+
def __enter__(self):
|
|
58
|
+
"""Enters the runtime context related to this object."""
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
62
|
+
"""Exits the runtime context related to this object."""
|
|
63
|
+
self.close_session()
|
|
64
|
+
|
|
65
|
+
def close_session(self) -> None:
|
|
66
|
+
"""Closes the current session(s)."""
|
|
67
|
+
if not self._is_session_closed:
|
|
68
|
+
self.__session.close()
|
|
69
|
+
self._is_session_closed = True
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_session_closed(self) -> bool:
|
|
73
|
+
"""Checks if the session is closed."""
|
|
74
|
+
return self._is_session_closed
|
|
75
|
+
|
|
76
|
+
def get_currently_playing(self, username: str) -> Optional[UserPlaying]:
|
|
77
|
+
"""
|
|
78
|
+
Fetches information about the currently playing or most recent track for a user.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
username : str
|
|
83
|
+
The Last.fm username to fetch data for.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Optional[UserPlaying_]
|
|
88
|
+
An instance of the ``UserPlaying`` model containing details about the currently
|
|
89
|
+
playing track if available, or ``None`` if the request fails or no data is available.
|
|
90
|
+
"""
|
|
91
|
+
query = f"?method=user.getrecenttracks&user={username}&limit=1&api_key={self.api_key}&format=json"
|
|
92
|
+
query_url = self.__api_url + query
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
response = self.__session.get(query_url, timeout=30)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
except requests.RequestException as e:
|
|
98
|
+
logger.error(f"Failed to fetch user profile: {e}")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
response_json = response.json()
|
|
102
|
+
result = response_json.get("recenttracks", {}).get("track", [])[0]
|
|
103
|
+
album_art = [
|
|
104
|
+
img.get("#text")
|
|
105
|
+
for img in result.get("image", [])
|
|
106
|
+
if img.get("size") == "extralarge"
|
|
107
|
+
]
|
|
108
|
+
return UserPlaying(
|
|
109
|
+
album_art="".join(album_art),
|
|
110
|
+
album_title=result.get("album", {}).get("#text"),
|
|
111
|
+
artists=", ".join(separate_artists(result.get("artist", {}).get("#text"))),
|
|
112
|
+
id=result.get("mbid"),
|
|
113
|
+
title=result.get("name"),
|
|
114
|
+
url=result.get("url"),
|
|
115
|
+
is_playing=result.get("@attr", {}).get("nowplaying", False),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
with LastFm() as lastfm:
|
|
121
|
+
username = input("Enter Lasfm Username: ").strip()
|
|
122
|
+
result = lastfm.get_currently_playing(username=username, limit=5)
|
|
123
|
+
|
|
124
|
+
if result:
|
|
125
|
+
pprint(asdict(result))
|
|
126
|
+
else:
|
|
127
|
+
print("No result was found. Make sure the username is correct!")
|
yutipy/spotify.py
CHANGED
|
@@ -192,7 +192,7 @@ class Spotify:
|
|
|
192
192
|
response_json["requested_at"] = time()
|
|
193
193
|
return response_json
|
|
194
194
|
else:
|
|
195
|
-
raise
|
|
195
|
+
raise AuthenticationException(
|
|
196
196
|
f"Invalid response received: {response.json()}"
|
|
197
197
|
)
|
|
198
198
|
|
|
@@ -814,6 +814,9 @@ class SpotifyAuth:
|
|
|
814
814
|
|
|
815
815
|
def __refresh_access_token(self):
|
|
816
816
|
"""Refreshes the token if it has expired."""
|
|
817
|
+
if not self.__access_token:
|
|
818
|
+
raise SpotifyAuthException("No access token was found.")
|
|
819
|
+
|
|
817
820
|
if time() - self.__token_requested_at >= self.__token_expires_in:
|
|
818
821
|
token_info = self.__get_access_token(refresh_token=self.__refresh_token)
|
|
819
822
|
|
|
@@ -844,7 +847,7 @@ class SpotifyAuth:
|
|
|
844
847
|
"""
|
|
845
848
|
return secrets.token_urlsafe(16)
|
|
846
849
|
|
|
847
|
-
def get_authorization_url(self, state: str = None):
|
|
850
|
+
def get_authorization_url(self, state: str = None, show_dialog: bool = False):
|
|
848
851
|
"""
|
|
849
852
|
Constructs the Spotify authorization URL for user authentication.
|
|
850
853
|
|
|
@@ -858,6 +861,11 @@ class SpotifyAuth:
|
|
|
858
861
|
If not provided, no state parameter is included.
|
|
859
862
|
|
|
860
863
|
You may use :meth:`SpotifyAuth.generate_state` method to generate one.
|
|
864
|
+
show_dialog : bool, optional
|
|
865
|
+
Whether or not to force the user to approve the app again if they’ve already done so.
|
|
866
|
+
If ``False`` (default), a user who has already approved the application may be automatically
|
|
867
|
+
redirected to the URI specified by redirect_uri. If ``True``, the user will not be automatically
|
|
868
|
+
redirected and will have to approve the app again.
|
|
861
869
|
|
|
862
870
|
Returns
|
|
863
871
|
-------
|
|
@@ -869,6 +877,7 @@ class SpotifyAuth:
|
|
|
869
877
|
"response_type": "code",
|
|
870
878
|
"client_id": self.client_id,
|
|
871
879
|
"redirect_uri": self.redirect_uri,
|
|
880
|
+
"show_dialog": show_dialog,
|
|
872
881
|
}
|
|
873
882
|
|
|
874
883
|
if self.scope:
|
|
@@ -1012,7 +1021,14 @@ class SpotifyAuth:
|
|
|
1012
1021
|
dict
|
|
1013
1022
|
A dictionary containing the user's display name and profile images.
|
|
1014
1023
|
"""
|
|
1015
|
-
|
|
1024
|
+
try:
|
|
1025
|
+
self.__refresh_access_token()
|
|
1026
|
+
except SpotifyAuthException:
|
|
1027
|
+
logger.warning(
|
|
1028
|
+
"No access token was found. You may authenticate the user again."
|
|
1029
|
+
)
|
|
1030
|
+
return None
|
|
1031
|
+
|
|
1016
1032
|
query_url = self.__api_url
|
|
1017
1033
|
header = self.__authorization_header()
|
|
1018
1034
|
|
|
@@ -1054,7 +1070,14 @@ class SpotifyAuth:
|
|
|
1054
1070
|
- If the API response does not contain the expected data, the method will return `None`.
|
|
1055
1071
|
|
|
1056
1072
|
"""
|
|
1057
|
-
|
|
1073
|
+
try:
|
|
1074
|
+
self.__refresh_access_token()
|
|
1075
|
+
except SpotifyAuthException:
|
|
1076
|
+
logger.warning(
|
|
1077
|
+
"No access token was found. You may authenticate the user again."
|
|
1078
|
+
)
|
|
1079
|
+
return None
|
|
1080
|
+
|
|
1058
1081
|
query_url = f"{self.__api_url}/player/currently-playing"
|
|
1059
1082
|
header = self.__authorization_header()
|
|
1060
1083
|
|
|
@@ -1064,8 +1087,16 @@ class SpotifyAuth:
|
|
|
1064
1087
|
except requests.RequestException as e:
|
|
1065
1088
|
raise NetworkException(f"Network error occurred: {e}")
|
|
1066
1089
|
|
|
1090
|
+
if response.status_code == 204:
|
|
1091
|
+
logger.info("Requested user is currently not listening to any music.")
|
|
1092
|
+
return None
|
|
1067
1093
|
if response.status_code != 200:
|
|
1068
|
-
|
|
1094
|
+
try:
|
|
1095
|
+
logger.error(f"Unexpected response: {response.json()}")
|
|
1096
|
+
except requests.exceptions.JSONDecodeError:
|
|
1097
|
+
logger.error(
|
|
1098
|
+
f"Response Code: {response.status_code}, Reason: {response.reason}"
|
|
1099
|
+
)
|
|
1069
1100
|
return None
|
|
1070
1101
|
|
|
1071
1102
|
response_json = response.json()
|
|
@@ -1099,6 +1130,8 @@ class SpotifyAuth:
|
|
|
1099
1130
|
url=result.get("external_urls", {}).get("spotify"),
|
|
1100
1131
|
)
|
|
1101
1132
|
|
|
1133
|
+
return None
|
|
1134
|
+
|
|
1102
1135
|
|
|
1103
1136
|
if __name__ == "__main__":
|
|
1104
1137
|
import logging
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yutipy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: A simple package for retrieving music information from various music platforms APIs.
|
|
5
5
|
Author: Cheap Nightbot
|
|
6
6
|
Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
|
|
@@ -90,6 +90,7 @@ Feel free to request any music platform you would like me to add by opening an i
|
|
|
90
90
|
- `Deezer`: https://www.deezer.com
|
|
91
91
|
- `iTunes`: https://music.apple.com
|
|
92
92
|
- `KKBOX`: https://www.kkbox.com
|
|
93
|
+
- `Lastfm`: https://last.fm
|
|
93
94
|
- `Spotify`: https://spotify.com
|
|
94
95
|
- `YouTube Music`: https://music.youtube.com
|
|
95
96
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
yutipy/__init__.py,sha256=
|
|
1
|
+
yutipy/__init__.py,sha256=Zrw3cr_6khXp1IgQdZxGcUM9A64GYgPs-6rlqSukW5Q,294
|
|
2
2
|
yutipy/deezer.py,sha256=PTTTfeORh1HZ_ta7_Uu4YARouSknUnAxO9AQJPFm4v0,11402
|
|
3
|
-
yutipy/exceptions.py,sha256=
|
|
3
|
+
yutipy/exceptions.py,sha256=oMuhNfDJ2AFsM_fJn6sayxMqIJRY_ihHRmL0U2IK6qQ,1501
|
|
4
4
|
yutipy/itunes.py,sha256=fV7KLsXWvfM_97KwVwn_KfnWM7j0cVGE7RytvnDGlZM,7929
|
|
5
5
|
yutipy/kkbox.py,sha256=MuYpR_UTZQxeitn2rc0UUgiMVikIXcVWmns3eSVjd_g,18847
|
|
6
|
+
yutipy/lastfm.py,sha256=0adVGigS8Kqnu52k-ry5eqHR6koktgKBhCNI1riUMfk,4302
|
|
6
7
|
yutipy/logger.py,sha256=cHCjpDslVsBOnp7jluqrOOi4ekDIggPhbSfqHeIfT-U,1263
|
|
7
8
|
yutipy/models.py,sha256=_92e54uXXCw53oWZiNLBBai6C0InOZMJL7r8GJ5smbM,2215
|
|
8
9
|
yutipy/musicyt.py,sha256=6Vz8bI8hDNFoDKRh6GK90dGMRbn_d5d6eGPsaYogb_Y,9315
|
|
9
|
-
yutipy/spotify.py,sha256=
|
|
10
|
+
yutipy/spotify.py,sha256=mHa5C6LTzfWOb_6LJhGm_HX7yvWcY0q7acUfU_ofw4A,43508
|
|
10
11
|
yutipy/yutipy_music.py,sha256=cHJ95HxGILweVrnEacj8tTlU0NPxMpuDVMpngdX0mZQ,6558
|
|
11
12
|
yutipy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
yutipy/cli/config.py,sha256=
|
|
13
|
+
yutipy/cli/config.py,sha256=e5RIq6RxVxxzx30nKVMa06gwyQ258s7U0WA1xvJuR_0,4543
|
|
13
14
|
yutipy/cli/search.py,sha256=8SQw0bjRzRqAg-FuVz9aWjB2KBZqmCf38SyKAQ3rx5E,3025
|
|
14
15
|
yutipy/utils/__init__.py,sha256=AZaqvs6AJwnqwJuodbGnHu702WSUqc8plVC16SppOcU,239
|
|
15
16
|
yutipy/utils/helpers.py,sha256=W3g9iqoSygcFFCKCp2sk0NQrZOEG26wI2XuNi9pgAXE,5207
|
|
16
|
-
yutipy-2.
|
|
17
|
-
yutipy-2.
|
|
18
|
-
yutipy-2.
|
|
19
|
-
yutipy-2.
|
|
20
|
-
yutipy-2.
|
|
21
|
-
yutipy-2.
|
|
17
|
+
yutipy-2.2.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
|
|
18
|
+
yutipy-2.2.1.dist-info/METADATA,sha256=sVaTUKYsKzgOyN8I3LLkHzUIWIGGln3FjfnTARrzdX8,6522
|
|
19
|
+
yutipy-2.2.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
20
|
+
yutipy-2.2.1.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
|
|
21
|
+
yutipy-2.2.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
|
|
22
|
+
yutipy-2.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|