yutipy 1.0.0__tar.gz → 1.2.0__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 (51) hide show
  1. yutipy-1.2.0/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  2. yutipy-1.2.0/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. {yutipy-1.0.0 → yutipy-1.2.0}/.github/workflows/release.yml +0 -23
  4. {yutipy-1.0.0 → yutipy-1.2.0}/PKG-INFO +13 -1
  5. {yutipy-1.0.0 → yutipy-1.2.0}/README.md +12 -0
  6. {yutipy-1.0.0 → yutipy-1.2.0}/docs/api_reference.rst +15 -7
  7. yutipy-1.2.0/docs/available_platforms.rst +13 -0
  8. {yutipy-1.0.0 → yutipy-1.2.0}/docs/faq.rst +8 -2
  9. {yutipy-1.0.0 → yutipy-1.2.0}/docs/index.rst +4 -4
  10. {yutipy-1.0.0 → yutipy-1.2.0}/docs/usage_examples.rst +32 -3
  11. yutipy-1.2.0/tests/test_kkbox.py +45 -0
  12. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/exceptions.py +4 -0
  13. yutipy-1.2.0/yutipy/kkbox.py +373 -0
  14. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/spotify.py +36 -19
  15. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy.egg-info/PKG-INFO +13 -1
  16. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy.egg-info/SOURCES.txt +5 -0
  17. {yutipy-1.0.0 → yutipy-1.2.0}/.gitattributes +0 -0
  18. {yutipy-1.0.0 → yutipy-1.2.0}/.github/FUNDING.yml +0 -0
  19. {yutipy-1.0.0 → yutipy-1.2.0}/.github/workflows/pytest-unit-testing.yml +0 -0
  20. {yutipy-1.0.0 → yutipy-1.2.0}/.gitignore +0 -0
  21. {yutipy-1.0.0 → yutipy-1.2.0}/.readthedocs.yaml +0 -0
  22. {yutipy-1.0.0 → yutipy-1.2.0}/LICENSE +0 -0
  23. {yutipy-1.0.0 → yutipy-1.2.0}/MANIFEST.in +0 -0
  24. {yutipy-1.0.0 → yutipy-1.2.0}/docs/Makefile +0 -0
  25. {yutipy-1.0.0 → yutipy-1.2.0}/docs/_static/yutipy_header.png +0 -0
  26. {yutipy-1.0.0 → yutipy-1.2.0}/docs/_static/yutipy_logo.png +0 -0
  27. {yutipy-1.0.0 → yutipy-1.2.0}/docs/conf.py +0 -0
  28. {yutipy-1.0.0 → yutipy-1.2.0}/docs/installation.rst +0 -0
  29. {yutipy-1.0.0 → yutipy-1.2.0}/docs/make.bat +0 -0
  30. {yutipy-1.0.0 → yutipy-1.2.0}/docs/requirements.txt +0 -0
  31. {yutipy-1.0.0 → yutipy-1.2.0}/pyproject.toml +0 -0
  32. {yutipy-1.0.0 → yutipy-1.2.0}/requirements-dev.txt +0 -0
  33. {yutipy-1.0.0 → yutipy-1.2.0}/requirements.txt +0 -0
  34. {yutipy-1.0.0 → yutipy-1.2.0}/setup.cfg +0 -0
  35. {yutipy-1.0.0 → yutipy-1.2.0}/tests/__init__.py +0 -0
  36. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_deezer.py +0 -0
  37. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_itunes.py +0 -0
  38. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_models.py +0 -0
  39. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_musicyt.py +0 -0
  40. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_spotify.py +0 -0
  41. {yutipy-1.0.0 → yutipy-1.2.0}/tests/test_utils.py +0 -0
  42. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/__init__.py +0 -0
  43. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/deezer.py +0 -0
  44. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/itunes.py +0 -0
  45. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/models.py +0 -0
  46. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/musicyt.py +0 -0
  47. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/utils/__init__.py +0 -0
  48. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy/utils/cheap_utils.py +0 -0
  49. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy.egg-info/dependency_links.txt +0 -0
  50. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy.egg-info/requires.txt +0 -0
  51. {yutipy-1.0.0 → yutipy-1.2.0}/yutipy.egg-info/top_level.txt +0 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: CheapNightbot
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **Additional context**
27
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
@@ -94,26 +94,3 @@ jobs:
94
94
  gh release upload
95
95
  "$GITHUB_REF_NAME" dist/**
96
96
  --repo "$GITHUB_REPOSITORY"
97
- publish-to-testpypi:
98
- name: Publish Python 🐍 distribution 📦 to TestPyPI
99
- needs:
100
- - build
101
- runs-on: ubuntu-latest
102
-
103
- environment:
104
- name: testpypi-release
105
- url: https://test.pypi.org/project/yutipy
106
-
107
- permissions:
108
- id-token: write # IMPORTANT: mandatory for trusted publishing
109
-
110
- steps:
111
- - name: Download all the dists
112
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
113
- with:
114
- name: python-package-distributions
115
- path: dist/
116
- - name: Publish distribution 📦 to TestPyPI
117
- uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
118
- with:
119
- repository-url: https://test.pypi.org/legacy/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: yutipy
3
- Version: 1.0.0
3
+ Version: 1.2.0
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>
@@ -85,6 +85,7 @@ A _**simple**_ Python package for searching and retrieving music information fro
85
85
  ## Table of Contents
86
86
 
87
87
  - [Features](#features)
88
+ - [Available Music Platforms](#available-music-platforms)
88
89
  - [Installation](#installation)
89
90
  - [Usage Example](#usage-example)
90
91
  - [Contributing](#contributing)
@@ -97,6 +98,17 @@ A _**simple**_ Python package for searching and retrieving music information fro
97
98
  - It uses `RapidFuzz` to compare & return the best match so that you can be sure you got what you asked for without having to worry and doing all that work by yourself.
98
99
  - Retrieve detailed music information, including album art, release dates, lyrics, ISRC, and UPC codes.
99
100
 
101
+ ### Available Music Platforms
102
+
103
+ Right now, the following music platforms are available in yutipy for searching music. New platforms will be added in the future.
104
+ Feel free to request any music platform you would like me to add by opening an issue on [GitHub](https://github.com/CheapNightbot/yutipy/issues) or by emailing me.
105
+
106
+ - `Deezer`: https://www.deezer.com
107
+ - `iTunes`: https://music.apple.com
108
+ - `KKBOX`: https://www.kkbox.com
109
+ - `Spotify`: https://spotify.com
110
+ - `YouTube Music`: https://music.youtube.com
111
+
100
112
  ## Installation
101
113
 
102
114
  You can install the package using pip. Make sure you have Python 3.8 or higher installed.
@@ -28,6 +28,7 @@ A _**simple**_ Python package for searching and retrieving music information fro
28
28
  ## Table of Contents
29
29
 
30
30
  - [Features](#features)
31
+ - [Available Music Platforms](#available-music-platforms)
31
32
  - [Installation](#installation)
32
33
  - [Usage Example](#usage-example)
33
34
  - [Contributing](#contributing)
@@ -40,6 +41,17 @@ A _**simple**_ Python package for searching and retrieving music information fro
40
41
  - It uses `RapidFuzz` to compare & return the best match so that you can be sure you got what you asked for without having to worry and doing all that work by yourself.
41
42
  - Retrieve detailed music information, including album art, release dates, lyrics, ISRC, and UPC codes.
42
43
 
44
+ ### Available Music Platforms
45
+
46
+ Right now, the following music platforms are available in yutipy for searching music. New platforms will be added in the future.
47
+ Feel free to request any music platform you would like me to add by opening an issue on [GitHub](https://github.com/CheapNightbot/yutipy/issues) or by emailing me.
48
+
49
+ - `Deezer`: https://www.deezer.com
50
+ - `iTunes`: https://music.apple.com
51
+ - `KKBOX`: https://www.kkbox.com
52
+ - `Spotify`: https://spotify.com
53
+ - `YouTube Music`: https://music.youtube.com
54
+
43
55
  ## Installation
44
56
 
45
57
  You can install the package using pip. Make sure you have Python 3.8 or higher installed.
@@ -5,13 +5,6 @@ API Reference
5
5
  Main Classes
6
6
  =============
7
7
 
8
- The following classes are available for use, utilizing the APIs of their respective music streaming platforms:
9
-
10
- - ``Deezer``: https://www.deezer.com
11
- - ``iTunes``: https://music.apple.com
12
- - ``Spotify``: https://spotify.com
13
- - ``YouTube Music``: https://music.youtube.com
14
-
15
8
  Deezer
16
9
  ------
17
10
 
@@ -30,6 +23,15 @@ iTunes
30
23
  :noindex:
31
24
  :exclude-members: is_session_closed
32
25
 
26
+ KKBox
27
+ -------
28
+
29
+ .. autoclass:: yutipy.kkbox.KKBox
30
+ :members:
31
+ :inherited-members:
32
+ :noindex:
33
+ :exclude-members: is_session_closed
34
+
33
35
  Spotify
34
36
  -------
35
37
 
@@ -114,3 +116,9 @@ Exceptions
114
116
  :inherited-members:
115
117
  :noindex:
116
118
  :exclude-members: add_note, args, with_traceback
119
+
120
+ .. autoclass:: yutipy.exceptions.KKBoxException
121
+ :members:
122
+ :inherited-members:
123
+ :noindex:
124
+ :exclude-members: add_note, args, with_traceback
@@ -0,0 +1,13 @@
1
+ =========================
2
+ Available Music Platforms
3
+ =========================
4
+
5
+ Right now, the following music platforms are available in yutipy for searching music. New platforms will be added in the future.
6
+ Feel free to request any music platform you would like me to add by opening an issue on `GitHub <https://github.com/CheapNightbot/yutipy/issues>`_ or by emailing me.
7
+
8
+
9
+ - ``Deezer``: https://www.deezer.com
10
+ - ``iTunes``: https://music.apple.com
11
+ - ``KKBOX``: https://www.kkbox.com
12
+ - ``Spotify``: https://spotify.com
13
+ - ``YouTube Music``: https://music.youtube.com
@@ -58,12 +58,18 @@ obtained from the Spotify for Developers website. To do this, follow these steps
58
58
 
59
59
  .. code-block:: bash
60
60
 
61
- CLIENT_ID=<spotify_client_id>
62
- CLIENT_SECRET=<spotify_client_secret>
61
+ SPOTIFY_CLIENT_ID=<spotify_client_id>
62
+ SPOTIFY_CLIENT_SECRET=<spotify_client_secret>
63
63
  - Ensure you replace ``<spotify_client_id>`` and ``<spotify_client_secret>`` with the values you copied in steps :ref:`5 <step_5>` and :ref:`6 <step_6>` respectively.
64
64
 
65
65
  After completing these steps, you should be able to use the ``Spotify`` class and its methods as expected.
66
66
 
67
+ Why am I receiving a ``KKBoxException`` when trying to use the ``KKBox`` class?
68
+ -------------------------------------------------------------------------------
69
+
70
+ Unfortunately, it's the same case as with Spotify. You will need a ``Client ID`` and ``Client Secret``
71
+ obtained from the KKBOX for Developers website. Please visit https://developer.kkbox.com/ for more information.
72
+
67
73
  ----
68
74
 
69
75
  .. [#] There may be additional features in the future.
@@ -3,9 +3,8 @@
3
3
  :target: https://github.com/CheapNightbot/yutipy
4
4
  :align: center
5
5
 
6
- **yutipy** is a Python package for searching and retrieving music information from various music platforms,
7
- including Deezer, iTunes, Spotify, and YouTube Music. This documentation will help you get started with yutipy
8
- and provide detailed information about its features and usage.
6
+ **yutipy** is a Python package for searching and retrieving music information from various music platforms (see list of :doc:`available muisc platforms <available_platforms>`).
7
+ This documentation will help you get started with yutipy and provide detailed information about its features and usage.
9
8
 
10
9
  .. raw:: html
11
10
 
@@ -45,8 +44,9 @@ Get Started
45
44
  :maxdepth: 2
46
45
 
47
46
  installation
48
- usage_examples
47
+ available_platforms
49
48
  api_reference
49
+ usage_examples
50
50
  faq
51
51
 
52
52
  .. toctree::
@@ -32,17 +32,46 @@ iTunes
32
32
  result = itunes.search("Artist Name", "Song Title")
33
33
  print(result)
34
34
 
35
+
36
+ KKBOX
37
+ -------
38
+
39
+ 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:
40
+
41
+ .. admonition:: .env
42
+
43
+ .. code-block:: bash
44
+
45
+ KKBOX_CLIENT_ID=<your_kkbox_client_id>
46
+ KKBOX_CLIENT_SECRET=<your_kkbox_client_secret>
47
+
48
+ Alternatively, you can manually provide these values when creating an object of the `KKBox` class:
49
+
50
+ .. code-block:: python
51
+
52
+ from yutipy.kkbox import KKBox
53
+
54
+ kkbox = KKBox(client_id="your_kkbox_client_id", client_secret="your_kkbox_client_secret")
55
+
56
+ .. code-block:: python
57
+
58
+ from yutipy.kkbox import KKBox
59
+
60
+ with KKBox() as kkbox:
61
+ result = kkbox.search("Artist Name", "Song Title")
62
+ print(result)
63
+
35
64
  Spotify
36
65
  -------
37
66
 
38
- To use the Spotify API, you need to set the ``CLIENT_ID`` and ``CLIENT_SECRET`` for Spotify. You can do this by creating a ``.env`` file in the root directory of your project with the following content:
67
+ To use the Spotify API, you need to set the ``SPOTIFY_CLIENT_ID`` and ``SPOTIFY_CLIENT_SECRET`` for Spotify. You can do this by creating a ``.env`` file in the root directory of your project with the following content:
39
68
 
40
69
  .. admonition:: .env
41
70
 
42
71
  .. code-block:: bash
43
72
 
44
- CLIENT_ID=your_spotify_client_id
45
- CLIENT_SECRET=your_spotify_client_secret
73
+ SPOTIFY_CLIENT_ID=<your_spotify_client_id>
74
+ SPOTIFY_CLIENT_SECRET=<your_spotify_client_secret>
46
75
 
47
76
  Alternatively, you can manually provide these values when creating an object of the `Spotify` class:
48
77
 
@@ -0,0 +1,45 @@
1
+ import pytest
2
+ from pytest import raises
3
+
4
+ from yutipy.exceptions import KKBoxException, InvalidValueException
5
+ from yutipy.models import MusicInfo
6
+ from yutipy.kkbox import KKBox
7
+
8
+
9
+ @pytest.fixture(scope="module")
10
+ def kkbox():
11
+ try:
12
+ return KKBox()
13
+ except KKBoxException:
14
+ pytest.skip("KKBOX credentials not found")
15
+
16
+
17
+ def test_search(kkbox):
18
+ artist = "Porter Robinson"
19
+ song = "Shelter"
20
+ result = kkbox.search(artist, song)
21
+ assert result is not None
22
+ assert isinstance(result, MusicInfo)
23
+ assert result.title == song
24
+ assert artist in result.artists
25
+
26
+
27
+ def test_get_html_widget(kkbox):
28
+ html_widget = kkbox.get_html_widget(id="8rceGrek59bDS0HmQH", content_type="song")
29
+ assert html_widget is not None
30
+ assert isinstance(html_widget, str)
31
+ assert "https://widget.kkbox.com/" in html_widget
32
+
33
+ with raises(InvalidValueException):
34
+ kkbox.get_html_widget(id="8rceGrek59bDS0HmQH", content_type="track")
35
+
36
+ with raises(InvalidValueException):
37
+ kkbox.get_html_widget(id="8rceGrek59bDS0HmQH", content_type="song", territory="US")
38
+
39
+ with raises(InvalidValueException):
40
+ kkbox.get_html_widget(id="8rceGrek59bDS0HmQH", content_type="song", widget_lang="JP")
41
+
42
+
43
+ def test_close_session(kkbox):
44
+ kkbox.close_session()
45
+ assert kkbox.is_session_closed
@@ -50,3 +50,7 @@ class InvalidResponseException(YutipyException):
50
50
  """Exception raised for invalid responses from APIs."""
51
51
 
52
52
  pass
53
+
54
+
55
+ class KKBoxException(YutipyException):
56
+ """Exception raised for erros related to the KKBOX Open API."""
@@ -0,0 +1,373 @@
1
+ import base64
2
+ import os
3
+ import time
4
+ from pprint import pprint
5
+ from typing import Optional, Union
6
+
7
+ import requests
8
+ from dotenv import load_dotenv
9
+
10
+ from yutipy.exceptions import (
11
+ AuthenticationException,
12
+ InvalidResponseException,
13
+ InvalidValueException,
14
+ KKBoxException,
15
+ NetworkException,
16
+ )
17
+ from yutipy.models import MusicInfo
18
+ from yutipy.utils.cheap_utils import are_strings_similar, is_valid_string
19
+
20
+ load_dotenv()
21
+
22
+ KKBOX_CLIENT_ID = os.getenv("KKBOX_CLIENT_ID")
23
+ KKBOX_CLIENT_SECRET = os.getenv("KKBOX_CLIENT_SECRET")
24
+
25
+
26
+ class KKBox:
27
+ """
28
+ A class to interact with KKBOX Open API.
29
+
30
+ This class reads the ``KKBOX_CLIENT_ID`` and ``KKBOX_CLIENT_SECRET`` from environment variables or the ``.env`` file by default.
31
+ Alternatively, you can manually provide these values when creating an object.
32
+ """
33
+
34
+ def __init__(
35
+ self, client_id: str = KKBOX_CLIENT_ID, client_secret: str = KKBOX_CLIENT_SECRET
36
+ ) -> None:
37
+ """
38
+ Initializes the KKBox class and sets up the session.
39
+
40
+ Parameters
41
+ ----------
42
+ client_id : str, optional
43
+ The Client ID for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_ID`` from .env file.
44
+ client_secret : str, optional
45
+ The Client secret for the KKBOX Open API. Defaults to ``KKBOX_CLIENT_SECRET`` from .env file.
46
+ """
47
+ if not client_id or not client_secret:
48
+ raise KKBoxException(
49
+ "Failed to read `KKBOX_CLIENT_ID` and/or `KKBOX_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
50
+ )
51
+
52
+ self.client_id = client_id
53
+ self.client_secret = client_secret
54
+ self._session = requests.Session()
55
+ self.api_url = "https://api.kkbox.com/v1.1"
56
+ self.__header, self.__expires_in = self.__authenticate()
57
+ self.__start_time = time.time()
58
+ self._is_session_closed = False
59
+ self.valid_territories = ["HK", "JP", "MY", "SG", "TW"]
60
+
61
+ def __enter__(self):
62
+ """Enters the runtime context related to this object."""
63
+ return self
64
+
65
+ def __exit__(self, exc_type, exc_value, exc_traceback):
66
+ """Exits the runtime context related to this object."""
67
+ self._close_session()
68
+
69
+ def close_session(self) -> None:
70
+ """Closes the current session."""
71
+ if not self.is_session_closed:
72
+ self._session.close()
73
+ self._is_session_closed = True
74
+
75
+ @property
76
+ def is_session_closed(self) -> bool:
77
+ """Checks if the session is closed."""
78
+ return self._is_session_closed
79
+
80
+ def __authenticate(self) -> tuple:
81
+ """
82
+ Authenticates with the KKBOX Open API and returns the authorization header.
83
+
84
+ Returns
85
+ -------
86
+ dict
87
+ The authorization header.
88
+ """
89
+ try:
90
+ token, expires_in = self.__get_access_token()
91
+ return {"Authorization": f"Bearer {token}"}, expires_in
92
+ except Exception as e:
93
+ raise AuthenticationException(
94
+ "Failed to authenticate with KKBOX Open API"
95
+ ) from e
96
+
97
+ def __get_access_token(self) -> tuple:
98
+ """
99
+ Gets the KKBOX Open API token.
100
+
101
+ Returns
102
+ -------
103
+ str
104
+ The KKBOX Open API token.
105
+ """
106
+ auth_string = f"{self.client_id}:{self.client_secret}"
107
+ auth_base64 = base64.b64encode(auth_string.encode("utf-8")).decode("utf-8")
108
+
109
+ url = " https://account.kkbox.com/oauth2/token"
110
+ headers = {
111
+ "Authorization": f"Basic {auth_base64}",
112
+ "Content-Type": "application/x-www-form-urlencoded",
113
+ }
114
+ data = {"grant_type": "client_credentials"}
115
+
116
+ try:
117
+ response = self._session.post(
118
+ url=url, headers=headers, data=data, timeout=30
119
+ )
120
+ response.raise_for_status()
121
+ except requests.RequestException as e:
122
+ raise NetworkException(f"Network error occurred: {e}")
123
+
124
+ try:
125
+ response_json = response.json()
126
+ return response_json.get("access_token"), response_json.get("expires_in")
127
+ except (KeyError, ValueError) as e:
128
+ raise InvalidResponseException(f"Invalid response received: {e}")
129
+
130
+ def __refresh_token_if_expired(self):
131
+ """Refreshes the token if it has expired."""
132
+ if time.time() - self.__start_time >= self.__expires_in:
133
+ self.__header, self.__expires_in = self.__authenticate()
134
+ self.__start_time = time.time()
135
+
136
+ def search(
137
+ self, artist: str, song: str, territory: str = "TW"
138
+ ) -> Optional[MusicInfo]:
139
+ """
140
+ Searches for a song by artist and title.
141
+
142
+ Parameters
143
+ ----------
144
+ artist : str
145
+ The name of the artist.
146
+ song : str
147
+ The title of the song.
148
+ territory : str
149
+ Two-letter country codes from ISO 3166-1 alpha-2.
150
+ Allowed values: ``HK``, ``JP``, ``MY``, ``SG``, ``TW``.
151
+
152
+ Returns
153
+ -------
154
+ Optional[MusicInfo_]
155
+ The music information if found, otherwise None.
156
+ """
157
+ if not is_valid_string(artist) or not is_valid_string(song):
158
+ raise InvalidValueException(
159
+ "Artist and song names must be valid strings and can't be empty."
160
+ )
161
+
162
+ self.__refresh_token_if_expired()
163
+
164
+ query = f"?q={artist} - {song}&type=track,album&territory={territory}&limit=10"
165
+ query_url = f"{self.api_url}/search{query}"
166
+
167
+ try:
168
+ response = self._session.get(query_url, headers=self.__header, timeout=30)
169
+ response.raise_for_status()
170
+ except requests.RequestException as e:
171
+ raise NetworkException(f"Network error occurred: {e}")
172
+
173
+ if response.status_code != 200:
174
+ raise KKBoxException(f"Failed to search for music: {response.json()}")
175
+
176
+ return self._find_music_info(artist, song, response.json())
177
+
178
+ def get_html_widget(
179
+ self,
180
+ id: str,
181
+ content_type: str,
182
+ territory: str = "TW",
183
+ widget_lang: str = "EN",
184
+ autoplay: bool = False,
185
+ loop: bool = False,
186
+ ) -> str:
187
+ """
188
+ Return KKBOX HTML widget for "Playlist", "Album" or "Song". It does not return actual HTML code,
189
+ the URL returned can be used in an HTML ``iframe`` with the help of ``src`` attribute.
190
+
191
+ Parameters
192
+ ----------
193
+ id : str
194
+ ``ID`` of playlist, album or track.
195
+ content_type : str
196
+ Content type can be ``playlist``, ``album`` or ``song``.
197
+ territory : str, optional
198
+ Territory code, i.e. "TW", "HK", "JP", "SG", "MY", by default "TW"
199
+ widget_lang : str, optional
200
+ The display language of the widget. Can be "TC", "SC", "JA", "EN", "MS", by default "EN"
201
+ autoplay : bool, optional
202
+ Whether to start playing music automatically in widget, by default False
203
+ loop : bool, optional
204
+ Repeat/loop song(s), by default False
205
+
206
+ Returns
207
+ -------
208
+ str
209
+ KKBOX HTML widget URL.
210
+ """
211
+ valid_content_types = ["playlist", "album", "song"]
212
+ valid_widget_langs = ["TC", "SC", "JA", "EN", "MS"]
213
+ if content_type not in valid_content_types:
214
+ raise InvalidValueException(
215
+ f"`content_type` must be one of these: {valid_content_types} !"
216
+ )
217
+
218
+ if territory not in self.valid_territories:
219
+ raise InvalidValueException(
220
+ f"`territory` must be one of these: {self.valid_territories} !"
221
+ )
222
+
223
+ if widget_lang not in valid_widget_langs:
224
+ raise InvalidValueException(
225
+ f"`widget_lang` must be one of these: {valid_widget_langs} !"
226
+ )
227
+
228
+ return f"https://widget.kkbox.com/v1/?id={id}&type={content_type}&terr={territory}&lang={widget_lang}&autoplay={autoplay}&loop={loop}"
229
+
230
+ def _find_music_info(
231
+ self, artist: str, song: str, response_json: dict
232
+ ) -> Optional[MusicInfo]:
233
+ """
234
+ Finds the music information from the search results.
235
+
236
+ Parameters
237
+ ----------
238
+ artist : str
239
+ The name of the artist.
240
+ song : str
241
+ The title of the song.
242
+ response_json : dict
243
+ The JSON response from the API.
244
+
245
+ Returns
246
+ -------
247
+ Optional[MusicInfo]
248
+ The music information if found, otherwise None.
249
+ """
250
+ try:
251
+ for track in response_json["tracks"]["data"]:
252
+ music_info = self._find_track(song, artist, track)
253
+ if music_info:
254
+ return music_info
255
+ except KeyError:
256
+ pass
257
+
258
+ try:
259
+ for album in response_json["albums"]["data"]:
260
+ music_info = self._find_album(song, artist, album)
261
+ if music_info:
262
+ return music_info
263
+ except KeyError:
264
+ pass
265
+
266
+ return None
267
+
268
+ def _find_track(self, song: str, artist: str, track: dict) -> Optional[MusicInfo]:
269
+ """
270
+ Finds the track information from the search results.
271
+
272
+ Parameters
273
+ ----------
274
+ song : str
275
+ The title of the song.
276
+ artist : str
277
+ The name of the artist.
278
+ track : dict
279
+ A single track from the search results.
280
+ artist_ids : list
281
+ A list of artist IDs.
282
+
283
+ Returns
284
+ -------
285
+ Optional[MusicInfo]
286
+ The music information if found, otherwise None.
287
+ """
288
+ if not are_strings_similar(track["name"], song):
289
+ return None
290
+
291
+ artists_name = track["album"]["artist"]["name"]
292
+ matching_artists = (
293
+ artists_name if are_strings_similar(artists_name, artist) else None
294
+ )
295
+
296
+ if matching_artists:
297
+ return MusicInfo(
298
+ album_art=track["album"]["images"][2]["url"],
299
+ album_title=track["album"]["name"],
300
+ album_type=None,
301
+ artists=artists_name,
302
+ genre=None,
303
+ id=track["id"],
304
+ isrc=track["isrc"],
305
+ lyrics=None,
306
+ release_date=track["album"]["release_date"],
307
+ tempo=None,
308
+ title=track["name"],
309
+ type="track",
310
+ upc=None,
311
+ url=track["url"],
312
+ )
313
+
314
+ return None
315
+
316
+ def _find_album(self, song: str, artist: str, album: dict) -> Optional[MusicInfo]:
317
+ """
318
+ Finds the album information from the search results.
319
+
320
+ Parameters
321
+ ----------
322
+ song : str
323
+ The title of the song.
324
+ artist : str
325
+ The name of the artist.
326
+ album : dict
327
+ A single album from the search results.
328
+ artist_ids : list
329
+ A list of artist IDs.
330
+
331
+ Returns
332
+ -------
333
+ Optional[MusicInfo]
334
+ The music information if found, otherwise None.
335
+ """
336
+ if not are_strings_similar(album["name"], song):
337
+ return None
338
+
339
+ artists_name = album["artist"]["name"]
340
+ matching_artists = (
341
+ artists_name if are_strings_similar(artists_name, artist) else None
342
+ )
343
+
344
+ if matching_artists:
345
+ return MusicInfo(
346
+ album_art=album["images"][2]["url"],
347
+ album_title=album["name"],
348
+ album_type=None,
349
+ artists=artists_name,
350
+ genre=None,
351
+ id=album["id"],
352
+ isrc=None,
353
+ lyrics=None,
354
+ release_date=album["release_date"],
355
+ tempo=None,
356
+ title=album["name"],
357
+ type="album",
358
+ upc=None,
359
+ url=album["url"],
360
+ )
361
+
362
+ return None
363
+
364
+
365
+ if __name__ == "__main__":
366
+ kkbox = KKBox(KKBOX_CLIENT_ID, KKBOX_CLIENT_SECRET)
367
+
368
+ try:
369
+ artist_name = input("Artist Name: ")
370
+ song_name = input("Song Name: ")
371
+ pprint(kkbox.search(artist_name, song_name))
372
+ finally:
373
+ kkbox.close_session()
@@ -36,7 +36,9 @@ class Spotify:
36
36
  """
37
37
 
38
38
  def __init__(
39
- self, client_id: str = SPOTIFY_CLIENT_ID, client_secret: str = SPOTIFY_CLIENT_SECRET
39
+ self,
40
+ client_id: str = SPOTIFY_CLIENT_ID,
41
+ client_secret: str = SPOTIFY_CLIENT_SECRET,
40
42
  ) -> None:
41
43
  """
42
44
  Initializes the Spotify class and sets up the session.
@@ -50,7 +52,7 @@ class Spotify:
50
52
  """
51
53
  if not client_id or not client_secret:
52
54
  raise SpotifyException(
53
- "Failed to read `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
55
+ "Failed to read `SPOTIFY_CLIENT_ID` and/or `SPOTIFY_CLIENT_SECRET` from environment variables. Client ID and Client Secret must be provided."
54
56
  )
55
57
 
56
58
  self.client_id = client_id
@@ -157,22 +159,37 @@ class Spotify:
157
159
  "Artist and song names must be valid strings and can't be empty."
158
160
  )
159
161
 
160
- self.__refresh_token_if_expired()
162
+ music_info = None
163
+ queries = [
164
+ f"?q=artist:{artist} track:{song}&type=track&limit=10",
165
+ f"?q=artist:{artist} album:{song}&type=album&limit=10",
166
+ ]
161
167
 
162
- query = f"?q=artist:{artist} track:{song}&type=track&limit=10"
163
- query_url = f"{self.api_url}/search{query}"
168
+ for query in queries:
169
+ if music_info:
170
+ return music_info
164
171
 
165
- try:
166
- response = self._session.get(query_url, headers=self.__header, timeout=30)
167
- response.raise_for_status()
168
- except requests.RequestException as e:
169
- raise NetworkException(f"Network error occurred: {e}")
172
+ self.__refresh_token_if_expired()
170
173
 
171
- if response.status_code != 200:
172
- raise SpotifyException(f"Failed to search for music: {response.json()}")
174
+ query_url = f"{self.api_url}/search{query}"
173
175
 
174
- artist_ids = self._get_artists_ids(artist)
175
- return self._find_music_info(artist, song, response.json(), artist_ids)
176
+ try:
177
+ response = self._session.get(
178
+ query_url, headers=self.__header, timeout=30
179
+ )
180
+ response.raise_for_status()
181
+ except requests.RequestException as e:
182
+ raise NetworkException(f"Network error occurred: {e}")
183
+
184
+ if response.status_code != 200:
185
+ raise SpotifyException(f"Failed to search for music: {response.json()}")
186
+
187
+ artist_ids = self._get_artists_ids(artist)
188
+ music_info = self._find_music_info(
189
+ artist, song, response.json(), artist_ids
190
+ )
191
+
192
+ return music_info
176
193
 
177
194
  def search_advanced(
178
195
  self, artist: str, song: str, isrc: str = None, upc: str = None
@@ -282,7 +299,7 @@ class Spotify:
282
299
  """
283
300
  try:
284
301
  for track in response_json["tracks"]["items"]:
285
- music_info = self._find_tracks(song, artist, track, artist_ids)
302
+ music_info = self._find_track(song, artist, track, artist_ids)
286
303
  if music_info:
287
304
  return music_info
288
305
  except KeyError:
@@ -298,7 +315,7 @@ class Spotify:
298
315
 
299
316
  return None
300
317
 
301
- def _find_tracks(
318
+ def _find_track(
302
319
  self, song: str, artist: str, track: dict, artist_ids: list
303
320
  ) -> Optional[MusicInfo]:
304
321
  """
@@ -404,11 +421,11 @@ class Spotify:
404
421
 
405
422
 
406
423
  if __name__ == "__main__":
407
- Spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
424
+ spotify = Spotify(SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET)
408
425
 
409
426
  try:
410
427
  artist_name = input("Artist Name: ")
411
428
  song_name = input("Song Name: ")
412
- pprint(Spotify.search(artist_name, song_name))
429
+ pprint(spotify.search(artist_name, song_name))
413
430
  finally:
414
- Spotify.close_session()
431
+ spotify.close_session()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: yutipy
3
- Version: 1.0.0
3
+ Version: 1.2.0
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>
@@ -85,6 +85,7 @@ A _**simple**_ Python package for searching and retrieving music information fro
85
85
  ## Table of Contents
86
86
 
87
87
  - [Features](#features)
88
+ - [Available Music Platforms](#available-music-platforms)
88
89
  - [Installation](#installation)
89
90
  - [Usage Example](#usage-example)
90
91
  - [Contributing](#contributing)
@@ -97,6 +98,17 @@ A _**simple**_ Python package for searching and retrieving music information fro
97
98
  - It uses `RapidFuzz` to compare & return the best match so that you can be sure you got what you asked for without having to worry and doing all that work by yourself.
98
99
  - Retrieve detailed music information, including album art, release dates, lyrics, ISRC, and UPC codes.
99
100
 
101
+ ### Available Music Platforms
102
+
103
+ Right now, the following music platforms are available in yutipy for searching music. New platforms will be added in the future.
104
+ Feel free to request any music platform you would like me to add by opening an issue on [GitHub](https://github.com/CheapNightbot/yutipy/issues) or by emailing me.
105
+
106
+ - `Deezer`: https://www.deezer.com
107
+ - `iTunes`: https://music.apple.com
108
+ - `KKBOX`: https://www.kkbox.com
109
+ - `Spotify`: https://spotify.com
110
+ - `YouTube Music`: https://music.youtube.com
111
+
100
112
  ## Installation
101
113
 
102
114
  You can install the package using pip. Make sure you have Python 3.8 or higher installed.
@@ -8,10 +8,13 @@ pyproject.toml
8
8
  requirements-dev.txt
9
9
  requirements.txt
10
10
  .github/FUNDING.yml
11
+ .github/ISSUE_TEMPLATE/bug_report.md
12
+ .github/ISSUE_TEMPLATE/feature_request.md
11
13
  .github/workflows/pytest-unit-testing.yml
12
14
  .github/workflows/release.yml
13
15
  docs/Makefile
14
16
  docs/api_reference.rst
17
+ docs/available_platforms.rst
15
18
  docs/conf.py
16
19
  docs/faq.rst
17
20
  docs/index.rst
@@ -24,6 +27,7 @@ docs/_static/yutipy_logo.png
24
27
  tests/__init__.py
25
28
  tests/test_deezer.py
26
29
  tests/test_itunes.py
30
+ tests/test_kkbox.py
27
31
  tests/test_models.py
28
32
  tests/test_musicyt.py
29
33
  tests/test_spotify.py
@@ -32,6 +36,7 @@ yutipy/__init__.py
32
36
  yutipy/deezer.py
33
37
  yutipy/exceptions.py
34
38
  yutipy/itunes.py
39
+ yutipy/kkbox.py
35
40
  yutipy/models.py
36
41
  yutipy/musicyt.py
37
42
  yutipy/spotify.py
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