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 CHANGED
@@ -3,6 +3,7 @@ from yutipy import (
3
3
  exceptions,
4
4
  itunes,
5
5
  kkbox,
6
+ lastfm,
6
7
  logger,
7
8
  musicyt,
8
9
  spotify,
@@ -14,6 +15,7 @@ __all__ = [
14
15
  "exceptions",
15
16
  "itunes",
16
17
  "kkbox",
18
+ "lastfm",
17
19
  "logger",
18
20
  "musicyt",
19
21
  "spotify",
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 required environment variables and their instructions
16
- required_vars = {
17
- "SPOTIFY_CLIENT_ID": {
18
- "description": "Spotify Client ID",
19
- "url": "https://developer.spotify.com/dashboard",
20
- "instructions": """
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
- "SPOTIFY_CLIENT_SECRET": {
28
- "description": "Spotify Client Secret",
29
- "url": "https://developer.spotify.com/dashboard",
30
- "instructions": "See the steps above for Spotify Client ID.",
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
- "KKBOX_CLIENT_ID": {
33
- "description": "KKBox Client ID",
34
- "url": "https://developer.kkbox.com/",
35
- "instructions": """
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
- "KKBOX_CLIENT_SECRET": {
43
- "description": "KKBox Client Secret",
44
- "url": "https://developer.kkbox.com/",
45
- "instructions": "See the steps above for KKBox Client ID.",
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 required_vars.items():
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 erros related to the KKBOX Open API."""
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 InvalidResponseException(
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
- self.__refresh_access_token()
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
- self.__refresh_access_token()
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
- logger.error(f"Unexpected response: {response.json()}")
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.1.1
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=eURfdAFBvA9xF6cuUJUMWLf44LHxRQuXONfIPD-CkIQ,268
1
+ yutipy/__init__.py,sha256=Zrw3cr_6khXp1IgQdZxGcUM9A64GYgPs-6rlqSukW5Q,294
2
2
  yutipy/deezer.py,sha256=PTTTfeORh1HZ_ta7_Uu4YARouSknUnAxO9AQJPFm4v0,11402
3
- yutipy/exceptions.py,sha256=fur945x1Ibu7yeIPDRsOcujfVRRa5JHQw27dsOUreK4,1393
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=m9GV3xZ94jRKtajMo34BARuR4l2gP6sUfZ8CKsJQDWo,42135
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=6p69ZlT3ebUH5wzhd0iisLYKOnYX6xTSzoyrdBwGNo0,3230
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.1.1.dist-info/licenses/LICENSE,sha256=_89JsS2QnBG8tAb5-VWbJDj_uJ002zPJAYBJJdh3DPY,1071
17
- yutipy-2.1.1.dist-info/METADATA,sha256=86Xbqgk9pHIiGIQkZAMzaOKkUu4vmo8sadsoH1hAlQ8,6494
18
- yutipy-2.1.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
19
- yutipy-2.1.1.dist-info/entry_points.txt,sha256=BrgmanaPjQqKQ3Ip76JLcsPgGANtrBSURf5CNIxl1HA,106
20
- yutipy-2.1.1.dist-info/top_level.txt,sha256=t2A5V2_mUcfnHkbCy6tAQlb3909jDYU5GQgXtA4756I,7
21
- yutipy-2.1.1.dist-info/RECORD,,
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