yutipy 2.1.1__tar.gz → 2.2.1__tar.gz

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.

Files changed (63) hide show
  1. {yutipy-2.1.1 → yutipy-2.2.1}/PKG-INFO +2 -1
  2. {yutipy-2.1.1 → yutipy-2.2.1}/README.md +1 -0
  3. {yutipy-2.1.1 → yutipy-2.2.1}/docs/api_reference.rst +16 -1
  4. {yutipy-2.1.1 → yutipy-2.2.1}/docs/available_platforms.rst +1 -0
  5. {yutipy-2.1.1 → yutipy-2.2.1}/docs/cli.rst +1 -1
  6. {yutipy-2.1.1 → yutipy-2.2.1}/docs/usage_examples.rst +40 -8
  7. yutipy-2.2.1/tests/__init__.py +21 -0
  8. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_deezer.py +5 -14
  9. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_itunes.py +3 -8
  10. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_kkbox.py +3 -8
  11. yutipy-2.2.1/tests/test_lastfm.py +60 -0
  12. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_models.py +15 -1
  13. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_musicyt.py +3 -2
  14. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_spotify.py +4 -21
  15. {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_utils.py +24 -1
  16. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/__init__.py +2 -0
  17. yutipy-2.2.1/yutipy/cli/config.py +122 -0
  18. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/exceptions.py +5 -1
  19. yutipy-2.2.1/yutipy/lastfm.py +127 -0
  20. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/spotify.py +38 -5
  21. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/PKG-INFO +2 -1
  22. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/SOURCES.txt +2 -0
  23. yutipy-2.1.1/tests/__init__.py +0 -1
  24. yutipy-2.1.1/yutipy/cli/config.py +0 -86
  25. {yutipy-2.1.1 → yutipy-2.2.1}/.gitattributes +0 -0
  26. {yutipy-2.1.1 → yutipy-2.2.1}/.github/FUNDING.yml +0 -0
  27. {yutipy-2.1.1 → yutipy-2.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {yutipy-2.1.1 → yutipy-2.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  29. {yutipy-2.1.1 → yutipy-2.2.1}/.github/dependabot.yml +0 -0
  30. {yutipy-2.1.1 → yutipy-2.2.1}/.github/workflows/pytest-unit-testing.yml +0 -0
  31. {yutipy-2.1.1 → yutipy-2.2.1}/.github/workflows/release.yml +0 -0
  32. {yutipy-2.1.1 → yutipy-2.2.1}/.gitignore +0 -0
  33. {yutipy-2.1.1 → yutipy-2.2.1}/.readthedocs.yaml +0 -0
  34. {yutipy-2.1.1 → yutipy-2.2.1}/LICENSE +0 -0
  35. {yutipy-2.1.1 → yutipy-2.2.1}/MANIFEST.in +0 -0
  36. {yutipy-2.1.1 → yutipy-2.2.1}/docs/Makefile +0 -0
  37. {yutipy-2.1.1 → yutipy-2.2.1}/docs/_static/yutipy_header.png +0 -0
  38. {yutipy-2.1.1 → yutipy-2.2.1}/docs/_static/yutipy_logo.png +0 -0
  39. {yutipy-2.1.1 → yutipy-2.2.1}/docs/conf.py +0 -0
  40. {yutipy-2.1.1 → yutipy-2.2.1}/docs/faq.rst +0 -0
  41. {yutipy-2.1.1 → yutipy-2.2.1}/docs/index.rst +0 -0
  42. {yutipy-2.1.1 → yutipy-2.2.1}/docs/installation.rst +0 -0
  43. {yutipy-2.1.1 → yutipy-2.2.1}/docs/make.bat +0 -0
  44. {yutipy-2.1.1 → yutipy-2.2.1}/docs/requirements.txt +0 -0
  45. {yutipy-2.1.1 → yutipy-2.2.1}/pyproject.toml +0 -0
  46. {yutipy-2.1.1 → yutipy-2.2.1}/requirements-dev.txt +0 -0
  47. {yutipy-2.1.1 → yutipy-2.2.1}/requirements.txt +0 -0
  48. {yutipy-2.1.1 → yutipy-2.2.1}/setup.cfg +0 -0
  49. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/cli/__init__.py +0 -0
  50. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/cli/search.py +0 -0
  51. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/deezer.py +0 -0
  52. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/itunes.py +0 -0
  53. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/kkbox.py +0 -0
  54. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/logger.py +0 -0
  55. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/models.py +0 -0
  56. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/musicyt.py +0 -0
  57. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/utils/__init__.py +0 -0
  58. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/utils/helpers.py +0 -0
  59. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/yutipy_music.py +0 -0
  60. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/dependency_links.txt +0 -0
  61. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/entry_points.txt +0 -0
  62. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/requires.txt +0 -0
  63. {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/top_level.txt +0 -0
@@ -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
 
@@ -54,6 +54,7 @@ Feel free to request any music platform you would like me to add by opening an i
54
54
  - `Deezer`: https://www.deezer.com
55
55
  - `iTunes`: https://music.apple.com
56
56
  - `KKBOX`: https://www.kkbox.com
57
+ - `Lastfm`: https://last.fm
57
58
  - `Spotify`: https://spotify.com
58
59
  - `YouTube Music`: https://music.youtube.com
59
60
 
@@ -24,7 +24,7 @@ iTunes
24
24
  :exclude-members: is_session_closed
25
25
 
26
26
  KKBox
27
- -------
27
+ -----
28
28
 
29
29
  .. autoclass:: yutipy.kkbox.KKBox
30
30
  :members:
@@ -32,6 +32,15 @@ KKBox
32
32
  :noindex:
33
33
  :exclude-members: is_session_closed
34
34
 
35
+ Lastfm
36
+ ------
37
+
38
+ .. autoclass:: yutipy.lastfm.LastFm
39
+ :members:
40
+ :inherited-members:
41
+ :noindex:
42
+ :exclude-members: is_session_closed
43
+
35
44
  Spotify
36
45
  -------
37
46
 
@@ -150,6 +159,12 @@ Service Exceptions
150
159
  :noindex:
151
160
  :exclude-members: add_note, args, with_traceback
152
161
 
162
+ .. autoclass:: yutipy.exceptions.LastFmException
163
+ :members:
164
+ :inherited-members:
165
+ :noindex:
166
+ :exclude-members: add_note, args, with_traceback
167
+
153
168
  .. autoclass:: yutipy.exceptions.MusicYTException
154
169
  :members:
155
170
  :inherited-members:
@@ -9,5 +9,6 @@ Feel free to request any music platform you would like me to add by opening an i
9
9
  - ``Deezer``: https://www.deezer.com
10
10
  - ``iTunes``: https://music.apple.com
11
11
  - ``KKBOX``: https://www.kkbox.com
12
+ - ``Lastfm``: https://last.fm
12
13
  - ``Spotify``: https://spotify.com
13
14
  - ``YouTube Music``: https://music.youtube.com
@@ -35,4 +35,4 @@ Set up API keys interactively:
35
35
 
36
36
  yutipy-config
37
37
 
38
- The wizard will guide you through obtaining and setting up API keys for supported services like Spotify and KKBOX.
38
+ The wizard will guide you through obtaining and setting up API keys for supported services like KKBOX, Lastfm and Spotify.
@@ -5,10 +5,14 @@ Usage Examples
5
5
  Here's a quick example of how to use the **yutipy** package to search for a song:
6
6
 
7
7
  .. important::
8
- All examples here use the ``with`` context manager to initialize an instance of the respective class,
9
- as those classes internally use ``requests.Session()`` for making requests to APIs.
10
- This approach ensures that the session is automatically closed once you exit the context. Although using ``with`` is not mandatory,
11
- if you instantiate an object without it, you are responsible for closing the session after use by calling the ``close_session()`` method on that object.
8
+ - All examples here use the ``with`` context manager to initialize an instance of the respective class,
9
+ as those classes internally use ``requests.Session()`` for making requests to APIs.
10
+ This approach ensures that the session is automatically closed once you exit the context. Although using ``with`` is not mandatory,
11
+ if you instantiate an object without it, you are responsible for closing the session after use by calling the ``close_session()`` method on that object.
12
+
13
+ - Following examples suggest to create ``.env`` file for storing, for example api keys. However, when hosting or deplyoing your application on production,
14
+ it is suggested and you may store them in environment variable(s) provided by your hosting provider.
15
+
12
16
 
13
17
  CLI Tool
14
18
  --------
@@ -41,11 +45,11 @@ iTunes
41
45
  result = itunes.search("Artist Name", "Song Title")
42
46
  print(result)
43
47
 
44
-
45
48
  KKBOX
46
- -------
49
+ -----
47
50
 
48
- To use the KKBOX Open API, you need to set the ``KKBOX_CLIENT_ID`` and ``KKBOX_CLIENT_SECRET`` for KKBOX. You can do this by creating a ``.env`` file in the root directory of your project with the following content:
51
+ To use the KKBOX Open API, you need to set the ``KKBOX_CLIENT_ID`` and ``KKBOX_CLIENT_SECRET`` for KKBOX.
52
+ You can do this by creating a ``.env`` file in the root directory of your project with the following content:
49
53
 
50
54
  .. admonition:: .env
51
55
 
@@ -54,7 +58,7 @@ To use the KKBOX Open API, you need to set the ``KKBOX_CLIENT_ID`` and ``KKBOX_C
54
58
  KKBOX_CLIENT_ID=<your_kkbox_client_id>
55
59
  KKBOX_CLIENT_SECRET=<your_kkbox_client_secret>
56
60
 
57
- Alternatively, you can manually provide these values when creating an object of the `KKBox` class:
61
+ Alternatively, you can manually provide these values when creating an object of the ``KKBox`` class:
58
62
 
59
63
  .. code-block:: python
60
64
 
@@ -70,6 +74,34 @@ Alternatively, you can manually provide these values when creating an object of
70
74
  result = kkbox.search("Artist Name", "Song Title")
71
75
  print(result)
72
76
 
77
+ Lastfm
78
+ ------
79
+
80
+ To use and retreive information from Lastfm, you need Lastfm API Key and need to set ``LASTFM_API_KEY``.
81
+ You can do this by creating a ``.env`` file int the root directory of your project with the following content:
82
+
83
+ .. admonition:: .env
84
+
85
+ .. code-block:: bash
86
+
87
+ LASTFM_API_KEY=<your_lastfm_api_key>
88
+
89
+ Alternatively, you can manually provide these values when creating an object of the ``LastFm`` class:
90
+
91
+ .. code-block:: python
92
+
93
+ from yutipy.lastfm import LastFm
94
+
95
+ lastfm = LastFm(api_key="your_lastfm_api_key")
96
+
97
+ .. code-block:: python
98
+
99
+ from yutipy.lastfm import LastFm
100
+
101
+ with LastFm() as lastfm:
102
+ result = lastfm.get_currently_playing(username="username")
103
+ print(result)
104
+
73
105
  Spotify
74
106
  -------
75
107
 
@@ -0,0 +1,21 @@
1
+ # This file is intentionally left blank. /ᐠ。ꞈ。ᐟ\ 喵~ not anymore !
2
+
3
+
4
+ class BaseResponse:
5
+ """
6
+ A base mock response class for simulating HTTP responses in tests.
7
+
8
+ Attributes:
9
+ status_code (int): The HTTP status code of the response. Defaults to 200.
10
+
11
+ Methods:
12
+ raise_for_status(): Simulates the behavior of `requests.Response.raise_for_status()`
13
+ by doing nothing, indicating a successful response with no exceptions raised.
14
+ """
15
+
16
+ status_code = 200
17
+
18
+ @staticmethod
19
+ def raise_for_status():
20
+ """Simulates a successful response with no exceptions raised."""
21
+ pass
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
 
3
+ from tests import BaseResponse
3
4
  from yutipy.deezer import Deezer
4
5
  from yutipy.models import MusicInfo
5
6
 
@@ -9,13 +10,8 @@ def deezer():
9
10
  return Deezer()
10
11
 
11
12
 
12
- class MockSearchResponse:
13
- status_code = 200
14
-
15
- @staticmethod
16
- def raise_for_status():
17
- pass
18
-
13
+ # Mock response only for the search endpoint
14
+ class MockSearchResponse(BaseResponse):
19
15
  @staticmethod
20
16
  def json():
21
17
  return {
@@ -46,13 +42,8 @@ class MockSearchResponse:
46
42
  }
47
43
 
48
44
 
49
- class MockResponse:
50
- status_code = 200
51
-
52
- @staticmethod
53
- def raise_for_status():
54
- pass
55
-
45
+ # Mock response for requesting individual track or album
46
+ class MockResponse(BaseResponse):
56
47
  @staticmethod
57
48
  def json():
58
49
  return {
@@ -1,9 +1,10 @@
1
1
  import pytest
2
2
  from pytest import raises
3
3
 
4
+ from tests import BaseResponse
5
+ from yutipy.exceptions import InvalidValueException
4
6
  from yutipy.itunes import Itunes
5
7
  from yutipy.models import MusicInfo
6
- from yutipy.exceptions import InvalidValueException
7
8
 
8
9
 
9
10
  @pytest.fixture
@@ -11,13 +12,7 @@ def itunes():
11
12
  return Itunes()
12
13
 
13
14
 
14
- class MockResponse:
15
- status_code = 200
16
-
17
- @staticmethod
18
- def raise_for_status():
19
- pass
20
-
15
+ class MockResponse(BaseResponse):
21
16
  @staticmethod
22
17
  def json():
23
18
  return {
@@ -1,9 +1,10 @@
1
1
  import pytest
2
2
  from pytest import raises
3
3
 
4
+ from tests import BaseResponse
4
5
  from yutipy.exceptions import InvalidValueException
5
- from yutipy.models import MusicInfo
6
6
  from yutipy.kkbox import KKBox
7
+ from yutipy.models import MusicInfo
7
8
 
8
9
 
9
10
  @pytest.fixture(scope="module")
@@ -24,13 +25,7 @@ def kkbox():
24
25
  return kkbox_instance
25
26
 
26
27
 
27
- class MockResponse:
28
- status_code = 200
29
-
30
- @staticmethod
31
- def raise_for_status():
32
- pass
33
-
28
+ class MockResponse(BaseResponse):
34
29
  @staticmethod
35
30
  def json():
36
31
  return {
@@ -0,0 +1,60 @@
1
+ import pytest
2
+
3
+ from yutipy.lastfm import LastFm
4
+ from yutipy.models import UserPlaying
5
+ from tests import BaseResponse
6
+
7
+
8
+ @pytest.fixture
9
+ def lastfm():
10
+ return LastFm(api_key="test_api_key")
11
+
12
+
13
+ class MockResponse(BaseResponse):
14
+ @staticmethod
15
+ def json():
16
+ return {
17
+ "recenttracks": {
18
+ "track": [
19
+ {
20
+ "artist": {"mbid": "", "#text": "Test Artist"},
21
+ "image": [
22
+ {
23
+ "size": "small",
24
+ "#text": "https://example.com/image/small.jpg",
25
+ },
26
+ {
27
+ "size": "extralarge",
28
+ "#text": "https://example.com/image/extralarge.jpg",
29
+ },
30
+ ],
31
+ "mbid": "",
32
+ "album": {
33
+ "mbid": "",
34
+ "#text": "Test Album",
35
+ },
36
+ "name": "Test Track",
37
+ "url": "https://www.last.fm/music/test+track",
38
+ }
39
+ ]
40
+ }
41
+ }
42
+
43
+
44
+ @pytest.fixture
45
+ def mock_response(lastfm, monkeypatch):
46
+ def mock_get(*args, **kwargs):
47
+ return MockResponse()
48
+
49
+ monkeypatch.setattr(lastfm._LastFm__session, "get", mock_get)
50
+
51
+
52
+ def test_get_currently_playing(lastfm, mock_response):
53
+ username = "bob"
54
+ currently_playing = lastfm.get_currently_playing(username=username)
55
+ assert currently_playing is not None
56
+ assert isinstance(currently_playing, UserPlaying)
57
+ assert currently_playing.title == "Test Track"
58
+ assert currently_playing.album_title == "Test Album"
59
+ assert "extralarge" in currently_playing.album_art
60
+ assert currently_playing.is_playing is False
@@ -1,4 +1,4 @@
1
- from yutipy.models import MusicInfo
1
+ from yutipy.models import MusicInfo, MusicInfos, UserPlaying
2
2
 
3
3
 
4
4
  def test_music_info():
@@ -33,3 +33,17 @@ def test_music_info():
33
33
  assert music_info.type == "track"
34
34
  assert music_info.upc == "123456789012"
35
35
  assert music_info.url == "https://example.com/song"
36
+
37
+
38
+ def test_music_infos():
39
+ music_infos = MusicInfos(album_art_source="Example Source")
40
+
41
+ assert music_infos.album_art_source == "Example Source"
42
+ assert isinstance(music_infos.album_art_source, str)
43
+
44
+
45
+ def test_user_playing():
46
+ user_playing = UserPlaying(is_playing=False)
47
+
48
+ assert user_playing.is_playing is False
49
+ assert isinstance(user_playing.is_playing, bool)
@@ -1,8 +1,9 @@
1
1
  import pytest
2
2
  from pytest import raises
3
- from yutipy.musicyt import MusicYT
4
- from yutipy.models import MusicInfo
3
+
5
4
  from yutipy.exceptions import InvalidValueException
5
+ from yutipy.models import MusicInfo
6
+ from yutipy.musicyt import MusicYT
6
7
 
7
8
 
8
9
  @pytest.fixture
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
 
3
+ from tests import BaseResponse
3
4
  from yutipy.models import MusicInfo, UserPlaying
4
5
  from yutipy.spotify import Spotify, SpotifyAuth
5
6
 
@@ -45,13 +46,7 @@ def spotify_auth():
45
46
 
46
47
  # Custom class to be the mock return value of requests.get()
47
48
  # for `Spotify` class only ~
48
- class MockResponse:
49
- status_code = 200
50
-
51
- @staticmethod
52
- def raise_for_status():
53
- pass
54
-
49
+ class MockResponse(BaseResponse):
55
50
  @staticmethod
56
51
  def json():
57
52
  return {
@@ -184,13 +179,7 @@ def test_get_currently_playing(spotify_auth, monkeypatch):
184
179
  return {"Authorization": "Bearer test_token"}
185
180
 
186
181
  def mock_get(*args, **kwargs):
187
- class MockResponse:
188
- status_code = 200
189
-
190
- @staticmethod
191
- def raise_for_status():
192
- pass
193
-
182
+ class MockResponse(BaseResponse):
194
183
  @staticmethod
195
184
  def json():
196
185
  return {
@@ -242,13 +231,7 @@ def test_get_user_profile(spotify_auth, monkeypatch):
242
231
  return {"Authorization": "Bearer test_token"}
243
232
 
244
233
  def mock_get(*args, **kwargs):
245
- class MockResponse:
246
- status_code = 200
247
-
248
- @staticmethod
249
- def raise_for_status():
250
- pass # Simulates a successful response with no exceptions raised
251
-
234
+ class MockResponse(BaseResponse):
252
235
  @staticmethod
253
236
  def json():
254
237
  return {
@@ -7,7 +7,30 @@ def test_are_strings_similar():
7
7
  assert are_strings_similar("Hello World", "Hello", use_translation=False) is True
8
8
 
9
9
 
10
- def test_are_strings_similar_translation():
10
+ def test_are_strings_similar_translation(monkeypatch):
11
+ # Mock responses for translate_text
12
+ mock_responses = {
13
+ "ポーター": {
14
+ "source-text": "ポーター",
15
+ "source-language": "ja",
16
+ "destination-text": "Porter",
17
+ "destination-language": "en",
18
+ },
19
+ "Porter": {
20
+ "source-text": "Porter",
21
+ "source-language": "en",
22
+ "destination-text": "Porter",
23
+ "destination-language": "en",
24
+ },
25
+ }
26
+
27
+ def mock_translate_text(text, *args, **kwargs):
28
+ return mock_responses[text]
29
+
30
+ # Use monkeypatch to replace translate_text with the mock function
31
+ monkeypatch.setattr("yutipy.utils.helpers.translate_text", mock_translate_text)
32
+
33
+ # Run the test with the mocked translate_text
11
34
  assert are_strings_similar("ポーター", "Porter") is True
12
35
 
13
36
 
@@ -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",
@@ -0,0 +1,122 @@
1
+ import os
2
+ import webbrowser
3
+
4
+ from dotenv import load_dotenv, set_key
5
+
6
+
7
+ def run_config_wizard():
8
+ """Interactive configuration wizard for setting up API keys."""
9
+ print("Welcome to the yutipy Configuration Wizard!")
10
+ print("This wizard will help you set up your API keys for various services.\n")
11
+
12
+ # Load existing .env file if it exists
13
+ env_file = ".env"
14
+ load_dotenv(env_file)
15
+
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": """
23
+ 1. Go to your Spotify Developer Dashboard: https://developer.spotify.com/dashboard
24
+ 2. Create a new app and fill in the required details.
25
+ 3. Copy the "Client ID" and "Client Secret" from the app's settings.
26
+ 4. Paste them here when prompted.
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
+ },
34
+ },
35
+ "KKBox": {
36
+ "KKBOX_CLIENT_ID": {
37
+ "description": "KKBox Client ID",
38
+ "url": "https://developer.kkbox.com/",
39
+ "instructions": """
40
+ 1. Go to the KKBOX Developer Portal: https://developer.kkbox.com/
41
+ 2. Log in and create a new application.
42
+ 3. Copy the "Client ID" and "Client Secret" from the app's settings.
43
+ 4. Paste them here when prompted.
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
+ },
51
+ },
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
+ },
64
+ },
65
+ }
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
+
85
+ # Track whether the browser has already been opened for a service
86
+ browser_opened = set()
87
+
88
+ # Prompt the user for each variable in the selected service
89
+ for var, details in selected_service.items():
90
+ current_value = os.getenv(var)
91
+ if current_value:
92
+ print(f"{details['description']} is already set.")
93
+ continue
94
+
95
+ print(f"\n{details['description']} is missing.")
96
+ print(details["instructions"])
97
+
98
+ # Check if the browser has already been opened for this service
99
+ if details["url"] not in browser_opened:
100
+ open_browser = (
101
+ input(
102
+ f"Do you want to open the website to get your {details['description']}? (y/N): "
103
+ )
104
+ .strip()
105
+ .lower()
106
+ )
107
+ if open_browser == "y":
108
+ webbrowser.open(details["url"])
109
+ print(f"The website has been opened in your browser: {details['url']}")
110
+ browser_opened.add(details["url"]) # Mark this URL as opened
111
+
112
+ # Prompt the user to enter the value
113
+ new_value = input(f"Enter your {details['description']}: ").strip()
114
+ if new_value:
115
+ set_key(env_file, var, new_value)
116
+ print(f"{details['description']} has been saved to the .env file.")
117
+
118
+ print("\nConfiguration complete! Your API keys have been saved to the .env file.")
119
+
120
+
121
+ if __name__ == "__main__":
122
+ run_config_wizard()
@@ -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):
@@ -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!")
@@ -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
 
@@ -30,6 +30,7 @@ tests/__init__.py
30
30
  tests/test_deezer.py
31
31
  tests/test_itunes.py
32
32
  tests/test_kkbox.py
33
+ tests/test_lastfm.py
33
34
  tests/test_models.py
34
35
  tests/test_musicyt.py
35
36
  tests/test_spotify.py
@@ -39,6 +40,7 @@ yutipy/deezer.py
39
40
  yutipy/exceptions.py
40
41
  yutipy/itunes.py
41
42
  yutipy/kkbox.py
43
+ yutipy/lastfm.py
42
44
  yutipy/logger.py
43
45
  yutipy/models.py
44
46
  yutipy/musicyt.py
@@ -1 +0,0 @@
1
- # This file is intentionally left blank. /ᐠ。ꞈ。ᐟ\ 喵~
@@ -1,86 +0,0 @@
1
- import os
2
- import webbrowser
3
- from dotenv import load_dotenv, set_key
4
-
5
-
6
- def run_config_wizard():
7
- """Interactive configuration wizard for setting up API keys."""
8
- print("Welcome to the yutipy Configuration Wizard!")
9
- print("This wizard will help you set up your API keys for various services.\n")
10
-
11
- # Load existing .env file if it exists
12
- env_file = ".env"
13
- load_dotenv(env_file)
14
-
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": """
21
- 1. Go to your Spotify Developer Dashboard: https://developer.spotify.com/dashboard
22
- 2. Create a new app and fill in the required details.
23
- 3. Copy the "Client ID" and "Client Secret" from the app's settings.
24
- 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.",
31
- },
32
- "KKBOX_CLIENT_ID": {
33
- "description": "KKBox Client ID",
34
- "url": "https://developer.kkbox.com/",
35
- "instructions": """
36
- 1. Go to the KKBOX Developer Portal: https://developer.kkbox.com/
37
- 2. Log in and create a new application.
38
- 3. Copy the "Client ID" and "Client Secret" from the app's settings.
39
- 4. Paste them here when prompted.
40
- """,
41
- },
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.",
46
- },
47
- }
48
-
49
- # Track whether the browser has already been opened for a service
50
- browser_opened = set()
51
-
52
- # Prompt the user for each variable
53
- for var, details in required_vars.items():
54
- current_value = os.getenv(var)
55
- if current_value:
56
- print(f"{details['description']} is already set.")
57
- continue
58
-
59
- print(f"\n{details['description']} is missing.")
60
- print(details["instructions"])
61
-
62
- # Check if the browser has already been opened for this service
63
- if details["url"] not in browser_opened:
64
- open_browser = (
65
- input(
66
- f"Do you want to open the website to get your {details['description']}? (y/N): "
67
- )
68
- .strip()
69
- .lower()
70
- )
71
- if open_browser == "y":
72
- webbrowser.open(details["url"])
73
- print(f"The website has been opened in your browser: {details['url']}")
74
- browser_opened.add(details["url"]) # Mark this URL as opened
75
-
76
- # Prompt the user to enter the value
77
- new_value = input(f"Enter your {details['description']}: ").strip()
78
- if new_value:
79
- set_key(env_file, var, new_value)
80
- print(f"{details['description']} has been saved to the .env file.")
81
-
82
- print("\nConfiguration complete! Your API keys have been saved to the .env file.")
83
-
84
-
85
- if __name__ == "__main__":
86
- run_config_wizard()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes