yutipy 2.3.6__tar.gz → 2.4.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 (66) hide show
  1. {yutipy-2.3.6 → yutipy-2.4.0}/PKG-INFO +2 -1
  2. {yutipy-2.3.6 → yutipy-2.4.0}/README.md +1 -0
  3. {yutipy-2.3.6 → yutipy-2.4.0}/docs/api_reference.rst +9 -0
  4. {yutipy-2.3.6 → yutipy-2.4.0}/docs/available_platforms.rst +1 -0
  5. {yutipy-2.3.6 → yutipy-2.4.0}/docs/usage_examples.rst +13 -0
  6. yutipy-2.4.0/tests/test_listenbrainz.py +103 -0
  7. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/lastfm.py +6 -1
  8. yutipy-2.4.0/yutipy/listenbrainz.py +141 -0
  9. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/utils/helpers.py +1 -1
  10. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy.egg-info/PKG-INFO +2 -1
  11. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy.egg-info/SOURCES.txt +2 -0
  12. {yutipy-2.3.6 → yutipy-2.4.0}/.gitattributes +0 -0
  13. {yutipy-2.3.6 → yutipy-2.4.0}/.github/FUNDING.yml +0 -0
  14. {yutipy-2.3.6 → yutipy-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {yutipy-2.3.6 → yutipy-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {yutipy-2.3.6 → yutipy-2.4.0}/.github/dependabot.yml +0 -0
  17. {yutipy-2.3.6 → yutipy-2.4.0}/.github/workflows/pytest-unit-testing.yml +0 -0
  18. {yutipy-2.3.6 → yutipy-2.4.0}/.github/workflows/release.yml +0 -0
  19. {yutipy-2.3.6 → yutipy-2.4.0}/.gitignore +0 -0
  20. {yutipy-2.3.6 → yutipy-2.4.0}/.readthedocs.yaml +0 -0
  21. {yutipy-2.3.6 → yutipy-2.4.0}/LICENSE +0 -0
  22. {yutipy-2.3.6 → yutipy-2.4.0}/MANIFEST.in +0 -0
  23. {yutipy-2.3.6 → yutipy-2.4.0}/docs/Makefile +0 -0
  24. {yutipy-2.3.6 → yutipy-2.4.0}/docs/_static/yutipy_header.png +0 -0
  25. {yutipy-2.3.6 → yutipy-2.4.0}/docs/_static/yutipy_logo.png +0 -0
  26. {yutipy-2.3.6 → yutipy-2.4.0}/docs/cli.rst +0 -0
  27. {yutipy-2.3.6 → yutipy-2.4.0}/docs/conf.py +0 -0
  28. {yutipy-2.3.6 → yutipy-2.4.0}/docs/faq.rst +0 -0
  29. {yutipy-2.3.6 → yutipy-2.4.0}/docs/index.rst +0 -0
  30. {yutipy-2.3.6 → yutipy-2.4.0}/docs/installation.rst +0 -0
  31. {yutipy-2.3.6 → yutipy-2.4.0}/docs/make.bat +0 -0
  32. {yutipy-2.3.6 → yutipy-2.4.0}/docs/requirements.txt +0 -0
  33. {yutipy-2.3.6 → yutipy-2.4.0}/pyproject.toml +0 -0
  34. {yutipy-2.3.6 → yutipy-2.4.0}/requirements-dev.txt +0 -0
  35. {yutipy-2.3.6 → yutipy-2.4.0}/requirements.txt +0 -0
  36. {yutipy-2.3.6 → yutipy-2.4.0}/setup.cfg +0 -0
  37. {yutipy-2.3.6 → yutipy-2.4.0}/tests/__init__.py +0 -0
  38. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_deezer.py +0 -0
  39. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_itunes.py +0 -0
  40. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_kkbox.py +0 -0
  41. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_lastfm.py +0 -0
  42. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_lrclib.py +0 -0
  43. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_models.py +0 -0
  44. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_musicyt.py +0 -0
  45. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_spotify.py +0 -0
  46. {yutipy-2.3.6 → yutipy-2.4.0}/tests/test_utils.py +0 -0
  47. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/__init__.py +0 -0
  48. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/base_clients.py +0 -0
  49. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/cli/__init__.py +0 -0
  50. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/cli/config.py +0 -0
  51. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/cli/search.py +0 -0
  52. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/deezer.py +0 -0
  53. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/exceptions.py +0 -0
  54. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/itunes.py +0 -0
  55. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/kkbox.py +0 -0
  56. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/logger.py +0 -0
  57. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/lrclib.py +0 -0
  58. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/models.py +0 -0
  59. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/musicyt.py +0 -0
  60. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/spotify.py +0 -0
  61. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/utils/__init__.py +0 -0
  62. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy/yutipy_music.py +0 -0
  63. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy.egg-info/dependency_links.txt +0 -0
  64. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy.egg-info/entry_points.txt +0 -0
  65. {yutipy-2.3.6 → yutipy-2.4.0}/yutipy.egg-info/requires.txt +0 -0
  66. {yutipy-2.3.6 → yutipy-2.4.0}/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.6
3
+ Version: 2.4.0
4
4
  Summary: A simple Python package to interact with various music platforms APIs.
5
5
  Author: Cheap Nightbot
6
6
  Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
@@ -92,6 +92,7 @@ Feel free to request any music platform you would like me to add by opening an i
92
92
  - `iTunes`: https://music.apple.com
93
93
  - `KKBOX`: https://www.kkbox.com
94
94
  - `Lastfm`: https://last.fm
95
+ - `ListenBrainz`: https://listenbrainz.org
95
96
  - `Spotify`: https://spotify.com
96
97
  - `YouTube Music`: https://music.youtube.com
97
98
 
@@ -58,6 +58,7 @@ Feel free to request any music platform you would like me to add by opening an i
58
58
  - `iTunes`: https://music.apple.com
59
59
  - `KKBOX`: https://www.kkbox.com
60
60
  - `Lastfm`: https://last.fm
61
+ - `ListenBrainz`: https://listenbrainz.org
61
62
  - `Spotify`: https://spotify.com
62
63
  - `YouTube Music`: https://music.youtube.com
63
64
 
@@ -41,6 +41,15 @@ Lastfm
41
41
  :noindex:
42
42
  :exclude-members: is_session_closed
43
43
 
44
+ ListenBrainz
45
+ ------------
46
+
47
+ .. autoclass:: yutipy.listenbrainz.ListenBrainz
48
+ :members:
49
+ :inherited-members:
50
+ :noindex:
51
+ :exclude-members: is_session_closed
52
+
44
53
  LRCLIB
45
54
  ------
46
55
 
@@ -10,5 +10,6 @@ Feel free to request any music platform you would like me to add by opening an i
10
10
  - ``iTunes``: https://music.apple.com
11
11
  - ``KKBOX``: https://www.kkbox.com
12
12
  - ``Lastfm``: https://last.fm
13
+ - ``ListenBrainz``: https://listenbrainz.org
13
14
  - ``Spotify``: https://spotify.com
14
15
  - ``YouTube Music``: https://music.youtube.com
@@ -102,6 +102,19 @@ Alternatively, you can manually provide these values when creating an object of
102
102
  result = lastfm.get_currently_playing(username="username")
103
103
  print(result)
104
104
 
105
+ ListenBrainz
106
+ ------------
107
+
108
+ .. code-block:: python
109
+
110
+ from dataclasses import asdict
111
+ from yutipy.listenbrainz import ListenBrainz
112
+
113
+ with ListenBrainz() as listenbrainz:
114
+ result = listenbrainz.get_currently_playing(username="username")
115
+ if result:
116
+ print(asdict(result))
117
+
105
118
  Spotify
106
119
  -------
107
120
 
@@ -0,0 +1,103 @@
1
+ import pytest
2
+
3
+ from yutipy.listenbrainz import ListenBrainz
4
+ from tests import BaseResponse
5
+
6
+
7
+ @pytest.fixture
8
+ def listenbrainz():
9
+ return ListenBrainz()
10
+
11
+
12
+ class MockResponseUsers(BaseResponse):
13
+ @staticmethod
14
+ def json():
15
+ return {
16
+ "users": [
17
+ {"user_name": "test4test"},
18
+ {"user_name": "lb_test"},
19
+ {"user_name": "lb-test"},
20
+ {"user_name": "muz-test"},
21
+ {"user_name": "tes--"},
22
+ {"user_name": "lb_test_1"},
23
+ {"user_name": "suvid_test"},
24
+ {"user_name": "testins"},
25
+ {"user_name": "hemang-test"},
26
+ {"user_name": "Test4585"},
27
+ ]
28
+ }
29
+
30
+
31
+ class MockResponseActivity(BaseResponse):
32
+ @staticmethod
33
+ def json():
34
+ return {
35
+ "payload": {
36
+ "count": 1,
37
+ "listens": [
38
+ {
39
+ "playing_now": True,
40
+ "track_metadata": {
41
+ "additional_info": {
42
+ "duration": 269,
43
+ "media_player": "Some Music Player",
44
+ "origin_url": "https://music.youtube.com/watch?v=lYBUbBu4W08",
45
+ "submission_client": "Some Scrobbler Plugin",
46
+ },
47
+ "artist_name": "Artist",
48
+ "track_name": "Song",
49
+ },
50
+ }
51
+ ],
52
+ "playing_now": True,
53
+ "user_id": "Username",
54
+ }
55
+ }
56
+
57
+
58
+ @pytest.fixture
59
+ def mock_response_users(listenbrainz, monkeypatch):
60
+ def mock_get(*args, **kwargs):
61
+ return MockResponseUsers()
62
+
63
+ monkeypatch.setattr(listenbrainz._ListenBrainz__session, "get", mock_get)
64
+
65
+
66
+ @pytest.fixture
67
+ def mock_response_activity(listenbrainz, monkeypatch):
68
+ def mock_get(*args, **kwargs):
69
+ return MockResponseActivity()
70
+
71
+ monkeypatch.setattr(listenbrainz._ListenBrainz__session, "get", mock_get)
72
+
73
+
74
+ def test_find_user(listenbrainz, mock_response_users):
75
+ username = "Test4585"
76
+ user_name = listenbrainz.find_user(username=username)
77
+ assert user_name is not None
78
+ assert username == user_name
79
+
80
+ username = "potato"
81
+ user_name = listenbrainz.find_user(username=username)
82
+ assert user_name is None
83
+
84
+
85
+ def test_find_user_case_insensitive(listenbrainz, mock_response_users):
86
+ username = "muz-TEST"
87
+ user_name = listenbrainz.find_user(username=username)
88
+ assert user_name is not None
89
+ assert username.lower() == user_name.lower()
90
+
91
+
92
+ def test_get_currently_playing(listenbrainz, mock_response_activity):
93
+ username = "Username"
94
+ activity = listenbrainz.get_currently_playing(username)
95
+ assert activity.is_playing is True
96
+ assert activity.title
97
+ assert activity.artists
98
+ assert activity.url
99
+
100
+ def test_get_currently_playing_no_user(listenbrainz, mock_response_activity):
101
+ username = "Bob"
102
+ activity = listenbrainz.get_currently_playing(username)
103
+ assert activity is None
@@ -71,6 +71,11 @@ class LastFm:
71
71
  """
72
72
  Fetches the user profile information for the provided username.
73
73
 
74
+ Parameters
75
+ ----------
76
+ username : str
77
+ The Last.fm username to fetch profile information for.
78
+
74
79
  Returns
75
80
  -------
76
81
  dict
@@ -109,7 +114,7 @@ class LastFm:
109
114
 
110
115
  def get_currently_playing(self, username: str) -> Optional[UserPlaying]:
111
116
  """
112
- Fetches information about the currently playing or most recent track for a user.
117
+ Fetches information about the currently playing track for a user.
113
118
 
114
119
  Parameters
115
120
  ----------
@@ -0,0 +1,141 @@
1
+ __all__ = ["ListenBrainz"]
2
+
3
+ from typing import Optional
4
+ from time import time
5
+
6
+ import requests
7
+
8
+ from yutipy.logger import logger
9
+ from yutipy.models import UserPlaying
10
+ from yutipy.utils.helpers import are_strings_similar
11
+
12
+
13
+ class ListenBrainz:
14
+ """A class to interact with the ListenBrainz API for fetching user music data."""
15
+
16
+ def __init__(self):
17
+ self._is_session_closed = False
18
+ self.__api_url = "https://api.listenbrainz.org"
19
+ self.__session = requests.Session()
20
+
21
+ def __enter__(self):
22
+ return self
23
+
24
+ def __exit__(self, exc_type, exc_value, exc_traceback):
25
+ self.close_session()
26
+
27
+ def close_session(self):
28
+ """Closes the current session(s)."""
29
+ if not self._is_session_closed:
30
+ self.__session.close()
31
+ self._is_session_closed = True
32
+
33
+ @property
34
+ def is_session_closed(self) -> bool:
35
+ """Checks if the session is closed."""
36
+ return self._is_session_closed
37
+
38
+ def find_user(self, username: str) -> Optional[str]:
39
+ """
40
+ Whether profile with the provided username exists on the ListenBrainz.
41
+
42
+ It searches ListenBrainz for the provided username and fuzzy matches
43
+ the provided username with the username(s) returned from ListenBrainz API.
44
+
45
+ Parameters
46
+ ----------
47
+ username : str
48
+ The username to search.
49
+
50
+ Returns
51
+ -------
52
+ Optional[str]
53
+ The username returned by ListenBrainz API if user found,
54
+ or ``None`` if the user with provided username does not exist.
55
+ """
56
+ endpoint = "/1/search/users/"
57
+ url = self.__api_url + endpoint
58
+
59
+ try:
60
+ response = self.__session.get(
61
+ url=url, params={"search_term": username}, timeout=30
62
+ )
63
+ response.raise_for_status()
64
+ except requests.RequestException as e:
65
+ logger.warning(f"Failed to search for the user: {e}")
66
+ return
67
+
68
+ response_json = response.json()
69
+ users = response_json.get("users")
70
+
71
+ if not users:
72
+ return
73
+
74
+ for user in users:
75
+ user_name = user.get("user_name")
76
+ if are_strings_similar(
77
+ username, user_name, threshold=100, use_translation=False
78
+ ):
79
+ return user_name
80
+
81
+ def get_currently_playing(self, username: str) -> Optional[UserPlaying]:
82
+ """
83
+ Fetches information about the currently playing track for a user.
84
+
85
+ Parameters
86
+ ----------
87
+ username : str
88
+ The ListenBrainz username to fetch data for.
89
+
90
+ Returns
91
+ -------
92
+ Optional[UserPlaying_]
93
+ An instance of the ``UserPlaying`` model containing details about the currently
94
+ playing track if available, or ``None`` if the request fails or no data is available.
95
+ """
96
+ endpoint = f"/1/user/{username}/playing-now"
97
+ url = self.__api_url + endpoint
98
+
99
+ try:
100
+ response = self.__session.get(url=url, timeout=30)
101
+ response.raise_for_status()
102
+ except requests.RequestException as e:
103
+ logger.warning(f"Failed to retrieve listening activity: {e}")
104
+ return
105
+
106
+ response_json = response.json()
107
+ if not response_json:
108
+ logger.info(
109
+ f"It seems no activity found for `{username}`. Might be user does not exist or not no data available."
110
+ )
111
+ return
112
+
113
+ result = response_json.get("payload", {})
114
+ is_playing = result.get("playing_now", False)
115
+ user_id = result.get("user_id", "")
116
+
117
+ if username.lower() != user_id.lower():
118
+ return
119
+
120
+ if result and is_playing:
121
+ track_metadata = result.get("listens", [{}])[0].get("track_metadata", {})
122
+ return UserPlaying(
123
+ artists=track_metadata.get("artist_name"),
124
+ id=None,
125
+ timestamp=time(),
126
+ title=track_metadata.get("track_name"),
127
+ url=track_metadata.get("additional_info", {}).get("origin_url"),
128
+ is_playing=is_playing,
129
+ )
130
+ return None
131
+
132
+
133
+ if __name__ == "__main__":
134
+ with ListenBrainz() as listenbrainz:
135
+ username = input("Enter ListenBrainz Username: ").strip()
136
+ result = listenbrainz.find_user(username)
137
+
138
+ if result:
139
+ print(f"User found with username: {result}")
140
+ else:
141
+ print(f"No user with username '{username}' found!")
@@ -76,7 +76,7 @@ def are_strings_similar(
76
76
  Args:
77
77
  str1 (str): First string to compare.
78
78
  str2 (str): Second string to compare.
79
- threshold (int, optional): Similarity threshold. Defaults to 80.
79
+ threshold (int, optional): Similarity threshold. Defaults to 95.
80
80
  use_translation (bool, optional): Use translations to compare strings. Defaults to ``True``
81
81
  translation_session (requests.Session, optional): A `requests.Session` object to use for making the API request. If not provided, a new session will be created and closed within the function.
82
82
  Providing your own session can improve performance by reusing the same session for multiple requests. Don't forget to close the session afterwards.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yutipy
3
- Version: 2.3.6
3
+ Version: 2.4.0
4
4
  Summary: A simple Python package to interact with various music platforms APIs.
5
5
  Author: Cheap Nightbot
6
6
  Author-email: Cheap Nightbot <hi@cheapnightbot.slmail.me>
@@ -92,6 +92,7 @@ Feel free to request any music platform you would like me to add by opening an i
92
92
  - `iTunes`: https://music.apple.com
93
93
  - `KKBOX`: https://www.kkbox.com
94
94
  - `Lastfm`: https://last.fm
95
+ - `ListenBrainz`: https://listenbrainz.org
95
96
  - `Spotify`: https://spotify.com
96
97
  - `YouTube Music`: https://music.youtube.com
97
98
 
@@ -31,6 +31,7 @@ tests/test_deezer.py
31
31
  tests/test_itunes.py
32
32
  tests/test_kkbox.py
33
33
  tests/test_lastfm.py
34
+ tests/test_listenbrainz.py
34
35
  tests/test_lrclib.py
35
36
  tests/test_models.py
36
37
  tests/test_musicyt.py
@@ -43,6 +44,7 @@ yutipy/exceptions.py
43
44
  yutipy/itunes.py
44
45
  yutipy/kkbox.py
45
46
  yutipy/lastfm.py
47
+ yutipy/listenbrainz.py
46
48
  yutipy/logger.py
47
49
  yutipy/lrclib.py
48
50
  yutipy/models.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
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