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.
- {yutipy-2.1.1 → yutipy-2.2.1}/PKG-INFO +2 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/README.md +1 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/api_reference.rst +16 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/available_platforms.rst +1 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/cli.rst +1 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/usage_examples.rst +40 -8
- yutipy-2.2.1/tests/__init__.py +21 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_deezer.py +5 -14
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_itunes.py +3 -8
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_kkbox.py +3 -8
- yutipy-2.2.1/tests/test_lastfm.py +60 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_models.py +15 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_musicyt.py +3 -2
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_spotify.py +4 -21
- {yutipy-2.1.1 → yutipy-2.2.1}/tests/test_utils.py +24 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/__init__.py +2 -0
- yutipy-2.2.1/yutipy/cli/config.py +122 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/exceptions.py +5 -1
- yutipy-2.2.1/yutipy/lastfm.py +127 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/spotify.py +38 -5
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/PKG-INFO +2 -1
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/SOURCES.txt +2 -0
- yutipy-2.1.1/tests/__init__.py +0 -1
- yutipy-2.1.1/yutipy/cli/config.py +0 -86
- {yutipy-2.1.1 → yutipy-2.2.1}/.gitattributes +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/FUNDING.yml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/dependabot.yml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/workflows/pytest-unit-testing.yml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.github/workflows/release.yml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.gitignore +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/.readthedocs.yaml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/LICENSE +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/MANIFEST.in +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/Makefile +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/_static/yutipy_header.png +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/_static/yutipy_logo.png +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/conf.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/faq.rst +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/index.rst +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/installation.rst +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/make.bat +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/docs/requirements.txt +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/pyproject.toml +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/requirements-dev.txt +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/requirements.txt +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/setup.cfg +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/cli/__init__.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/cli/search.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/deezer.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/itunes.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/kkbox.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/logger.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/models.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/musicyt.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/utils/__init__.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/utils/helpers.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy/yutipy_music.py +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/dependency_links.txt +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/entry_points.txt +0 -0
- {yutipy-2.1.1 → yutipy-2.2.1}/yutipy.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
|
@@ -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
|
yutipy-2.1.1/tests/__init__.py
DELETED
|
@@ -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
|
|
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
|