yutipy 1.3.2__tar.gz → 1.4.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-1.3.2 → yutipy-1.4.1}/.github/workflows/release.yml +5 -3
- {yutipy-1.3.2 → yutipy-1.4.1}/PKG-INFO +2 -2
- {yutipy-1.3.2 → yutipy-1.4.1}/pyproject.toml +1 -1
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_deezer.py +7 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_musicyt.py +5 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_utils.py +8 -1
- yutipy-1.4.1/tests/test_yutipy_music.py +48 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/__init__.py +2 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/deezer.py +28 -5
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/itunes.py +25 -5
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/kkbox.py +45 -8
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/musicyt.py +40 -6
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/spotify.py +67 -14
- yutipy-1.4.1/yutipy/utils/__init__.py +13 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/utils/cheap_utils.py +44 -5
- yutipy-1.4.1/yutipy/utils/logger.py +4 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/yutipy_music.py +74 -26
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy.egg-info/PKG-INFO +2 -2
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy.egg-info/SOURCES.txt +3 -1
- yutipy-1.3.2/yutipy/utils/__init__.py +0 -7
- {yutipy-1.3.2 → yutipy-1.4.1}/.gitattributes +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.github/FUNDING.yml +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.github/workflows/pytest-unit-testing.yml +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.gitignore +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/.readthedocs.yaml +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/LICENSE +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/MANIFEST.in +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/README.md +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/Makefile +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/_static/yutipy_header.png +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/_static/yutipy_logo.png +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/api_reference.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/available_platforms.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/conf.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/faq.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/index.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/installation.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/make.bat +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/requirements.txt +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/docs/usage_examples.rst +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/requirements-dev.txt +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/requirements.txt +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/setup.cfg +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/__init__.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_itunes.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_kkbox.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_models.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/tests/test_spotify.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/exceptions.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy/models.py +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy.egg-info/dependency_links.txt +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy.egg-info/requires.txt +0 -0
- {yutipy-1.3.2 → yutipy-1.4.1}/yutipy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
name: Publish Python 🐍 distribution 📦 to PyPI
|
|
1
|
+
name: Publish Python 🐍 distribution 📦 to PyPI
|
|
2
2
|
|
|
3
|
-
on:
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '*'
|
|
4
7
|
|
|
5
8
|
jobs:
|
|
6
9
|
build:
|
|
@@ -82,7 +85,6 @@ jobs:
|
|
|
82
85
|
gh release create
|
|
83
86
|
"$GITHUB_REF_NAME"
|
|
84
87
|
--repo "$GITHUB_REPOSITORY"
|
|
85
|
-
--notes ""
|
|
86
88
|
--generate-notes
|
|
87
89
|
- name: Upload artifact signatures to GitHub Release
|
|
88
90
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yutipy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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>
|
|
@@ -33,7 +33,7 @@ Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
|
|
|
33
33
|
Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
|
|
34
34
|
Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
|
|
35
35
|
Project-URL: funding, https://ko-fi.com/cheapnightbot
|
|
36
|
-
Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify
|
|
36
|
+
Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify,KKBox
|
|
37
37
|
Classifier: Development Status :: 4 - Beta
|
|
38
38
|
Classifier: Intended Audience :: Developers
|
|
39
39
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -22,7 +22,7 @@ maintainers = [
|
|
|
22
22
|
description = "A simple package for retrieving music information from various music platforms APIs."
|
|
23
23
|
readme = "README.md"
|
|
24
24
|
license = {file = "LICENSE"}
|
|
25
|
-
keywords = ["music", "API", "Deezer", "iTunes", "Spotify", "YouTube Music", "search", "retrieve", "information", "yutify"]
|
|
25
|
+
keywords = ["music", "API", "Deezer", "iTunes", "Spotify", "YouTube Music", "search", "retrieve", "information", "yutify", "KKBox"]
|
|
26
26
|
classifiers = [
|
|
27
27
|
"Development Status :: 4 - Beta",
|
|
28
28
|
"Intended Audience :: Developers",
|
|
@@ -21,6 +21,13 @@ def test_search_invalid(deezer):
|
|
|
21
21
|
assert result is None
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def test_search_translation_matching(deezer):
|
|
25
|
+
result = deezer.search("Porter Robinson", "Shelter", normalize_non_english=False)
|
|
26
|
+
assert result.id != 1355346522
|
|
27
|
+
result = deezer.search("Porter Robinson", "Shelter", normalize_non_english=True)
|
|
28
|
+
assert result.id == 1355346522 or 2404408865
|
|
29
|
+
|
|
30
|
+
|
|
24
31
|
def test_get_upc_isrc_track(deezer):
|
|
25
32
|
track_id = 781592622
|
|
26
33
|
result = deezer._get_upc_isrc(track_id, "track")
|
|
@@ -7,10 +7,17 @@ def test_are_strings_similar():
|
|
|
7
7
|
assert are_strings_similar("Hello World", "Hello") is True
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def test_are_strings_similar_translation():
|
|
11
|
+
assert are_strings_similar("ポーター", "Porter") is True
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
def test_separate_artists():
|
|
11
15
|
assert separate_artists("Artist A & Artist B") == ["Artist A", "Artist B"]
|
|
12
16
|
assert separate_artists("Artist A ft. Artist B") == ["Artist A", "Artist B"]
|
|
13
17
|
assert separate_artists("Artist A") == ["Artist A"]
|
|
14
18
|
assert separate_artists("Artist A and Artist B") == ["Artist A", "Artist B"]
|
|
15
19
|
assert separate_artists("Artist A / Artist B") == ["Artist A", "Artist B"]
|
|
16
|
-
assert separate_artists("Artist A, Artist B", custom_separator=",") == [
|
|
20
|
+
assert separate_artists("Artist A, Artist B", custom_separator=",") == [
|
|
21
|
+
"Artist A",
|
|
22
|
+
"Artist B",
|
|
23
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from yutipy.yutipy_music import YutipyMusic
|
|
3
|
+
from yutipy.models import MusicInfos
|
|
4
|
+
from yutipy.exceptions import InvalidValueException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def yutipy_music():
|
|
9
|
+
return YutipyMusic()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_search_valid(yutipy_music):
|
|
13
|
+
artist = "Adele"
|
|
14
|
+
song = "Hello"
|
|
15
|
+
result = yutipy_music.search(artist, song)
|
|
16
|
+
assert result is not None
|
|
17
|
+
assert isinstance(result, MusicInfos)
|
|
18
|
+
assert "Hello" in result.title
|
|
19
|
+
assert "Adele" in result.artists
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_search_invalid(yutipy_music):
|
|
23
|
+
artist = "Nonexistent Artist"
|
|
24
|
+
song = "Nonexistent Song"
|
|
25
|
+
result = yutipy_music.search(artist, song)
|
|
26
|
+
assert result is None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_search_empty_artist(yutipy_music):
|
|
30
|
+
artist = ""
|
|
31
|
+
song = "Hello"
|
|
32
|
+
|
|
33
|
+
with pytest.raises(InvalidValueException):
|
|
34
|
+
yutipy_music.search(artist, song)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_search_empty_song(yutipy_music):
|
|
38
|
+
artist = "Adele"
|
|
39
|
+
song = ""
|
|
40
|
+
|
|
41
|
+
with pytest.raises(InvalidValueException):
|
|
42
|
+
yutipy_music.search(artist, song)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_close_sessions(yutipy_music):
|
|
46
|
+
yutipy_music.close_sessions()
|
|
47
|
+
for service in yutipy_music.services.values():
|
|
48
|
+
assert service.is_session_closed
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .deezer import Deezer
|
|
2
2
|
from .itunes import Itunes
|
|
3
|
+
from .kkbox import KKBox
|
|
3
4
|
from .models import MusicInfo
|
|
4
5
|
from .musicyt import MusicYT
|
|
5
6
|
from .spotify import Spotify
|
|
@@ -8,6 +9,7 @@ from .yutipy_music import YutipyMusic
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"Deezer",
|
|
10
11
|
"Itunes",
|
|
12
|
+
"KKBox",
|
|
11
13
|
"MusicInfo",
|
|
12
14
|
"MusicYT",
|
|
13
15
|
"Spotify",
|
|
@@ -21,6 +21,8 @@ class Deezer:
|
|
|
21
21
|
self._session = requests.Session()
|
|
22
22
|
self.api_url = "https://api.deezer.com"
|
|
23
23
|
self._is_session_closed = False
|
|
24
|
+
self.normalize_non_english = True
|
|
25
|
+
self._translation_session = requests.Session()
|
|
24
26
|
|
|
25
27
|
def __enter__(self) -> "Deezer":
|
|
26
28
|
"""Enters the runtime context related to this object."""
|
|
@@ -34,6 +36,7 @@ class Deezer:
|
|
|
34
36
|
"""Closes the current session."""
|
|
35
37
|
if not self.is_session_closed:
|
|
36
38
|
self._session.close()
|
|
39
|
+
self._translation_session.close()
|
|
37
40
|
self._is_session_closed = True
|
|
38
41
|
|
|
39
42
|
@property
|
|
@@ -41,7 +44,13 @@ class Deezer:
|
|
|
41
44
|
"""Checks if the session is closed."""
|
|
42
45
|
return self._is_session_closed
|
|
43
46
|
|
|
44
|
-
def search(
|
|
47
|
+
def search(
|
|
48
|
+
self,
|
|
49
|
+
artist: str,
|
|
50
|
+
song: str,
|
|
51
|
+
limit: int = 10,
|
|
52
|
+
normalize_non_english: bool = True,
|
|
53
|
+
) -> Optional[MusicInfo]:
|
|
45
54
|
"""
|
|
46
55
|
Searches for a song by artist and title.
|
|
47
56
|
|
|
@@ -52,8 +61,10 @@ class Deezer:
|
|
|
52
61
|
song : str
|
|
53
62
|
The title of the song.
|
|
54
63
|
limit: int, optional
|
|
55
|
-
The number of items to retrieve from API.
|
|
56
|
-
|
|
64
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
|
|
65
|
+
normalize_non_english : bool, optional
|
|
66
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
67
|
+
|
|
57
68
|
|
|
58
69
|
Returns
|
|
59
70
|
-------
|
|
@@ -65,6 +76,8 @@ class Deezer:
|
|
|
65
76
|
"Artist and song names must be valid strings and can't be empty."
|
|
66
77
|
)
|
|
67
78
|
|
|
79
|
+
self.normalize_non_english = normalize_non_english
|
|
80
|
+
|
|
68
81
|
search_types = ["track", "album"]
|
|
69
82
|
|
|
70
83
|
for search_type in search_types:
|
|
@@ -208,8 +221,18 @@ class Deezer:
|
|
|
208
221
|
"""
|
|
209
222
|
for result in results:
|
|
210
223
|
if not (
|
|
211
|
-
are_strings_similar(
|
|
212
|
-
|
|
224
|
+
are_strings_similar(
|
|
225
|
+
result["title"],
|
|
226
|
+
song,
|
|
227
|
+
use_translation=self.normalize_non_english,
|
|
228
|
+
translation_session=self._translation_session,
|
|
229
|
+
)
|
|
230
|
+
and are_strings_similar(
|
|
231
|
+
result["artist"]["name"],
|
|
232
|
+
artist,
|
|
233
|
+
use_translation=self.normalize_non_english,
|
|
234
|
+
translation_session=self._translation_session,
|
|
235
|
+
)
|
|
213
236
|
):
|
|
214
237
|
continue
|
|
215
238
|
|
|
@@ -22,6 +22,8 @@ class Itunes:
|
|
|
22
22
|
self._session = requests.Session()
|
|
23
23
|
self.api_url = "https://itunes.apple.com"
|
|
24
24
|
self._is_session_closed = False
|
|
25
|
+
self.normalize_non_english = True
|
|
26
|
+
self._translation_session = requests.Session()
|
|
25
27
|
|
|
26
28
|
def __enter__(self) -> "Itunes":
|
|
27
29
|
"""Enters the runtime context related to this object."""
|
|
@@ -35,6 +37,7 @@ class Itunes:
|
|
|
35
37
|
"""Closes the current session."""
|
|
36
38
|
if not self.is_session_closed:
|
|
37
39
|
self._session.close()
|
|
40
|
+
self._translation_session.close()
|
|
38
41
|
self._is_session_closed = True
|
|
39
42
|
|
|
40
43
|
@property
|
|
@@ -42,7 +45,13 @@ class Itunes:
|
|
|
42
45
|
"""Checks if the session is closed."""
|
|
43
46
|
return self._is_session_closed
|
|
44
47
|
|
|
45
|
-
def search(
|
|
48
|
+
def search(
|
|
49
|
+
self,
|
|
50
|
+
artist: str,
|
|
51
|
+
song: str,
|
|
52
|
+
limit: int = 10,
|
|
53
|
+
normalize_non_english: bool = True,
|
|
54
|
+
) -> Optional[MusicInfo]:
|
|
46
55
|
"""
|
|
47
56
|
Searches for a song by artist and title.
|
|
48
57
|
|
|
@@ -53,8 +62,9 @@ class Itunes:
|
|
|
53
62
|
song : str
|
|
54
63
|
The title of the song.
|
|
55
64
|
limit: int, optional
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
|
|
66
|
+
normalize_non_english : bool, optional
|
|
67
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
58
68
|
|
|
59
69
|
Returns
|
|
60
70
|
-------
|
|
@@ -66,6 +76,8 @@ class Itunes:
|
|
|
66
76
|
"Artist and song names must be valid strings and can't be empty."
|
|
67
77
|
)
|
|
68
78
|
|
|
79
|
+
self.normalize_non_english = normalize_non_english
|
|
80
|
+
|
|
69
81
|
entities = ["song", "album"]
|
|
70
82
|
for entity in entities:
|
|
71
83
|
endpoint = f"{self.api_url}/search"
|
|
@@ -114,9 +126,17 @@ class Itunes:
|
|
|
114
126
|
for result in results:
|
|
115
127
|
if not (
|
|
116
128
|
are_strings_similar(
|
|
117
|
-
result.get("trackName", result["collectionName"]),
|
|
129
|
+
result.get("trackName", result["collectionName"]),
|
|
130
|
+
song,
|
|
131
|
+
use_translation=self.normalize_non_english,
|
|
132
|
+
translation_session=self._translation_session,
|
|
133
|
+
)
|
|
134
|
+
and are_strings_similar(
|
|
135
|
+
result["artistName"],
|
|
136
|
+
artist,
|
|
137
|
+
use_translation=self.normalize_non_english,
|
|
138
|
+
translation_session=self._translation_session,
|
|
118
139
|
)
|
|
119
|
-
and are_strings_similar(result["artistName"], artist)
|
|
120
140
|
):
|
|
121
141
|
continue
|
|
122
142
|
|
|
@@ -57,6 +57,8 @@ class KKBox:
|
|
|
57
57
|
self.__start_time = time.time()
|
|
58
58
|
self._is_session_closed = False
|
|
59
59
|
self.valid_territories = ["HK", "JP", "MY", "SG", "TW"]
|
|
60
|
+
self.normalize_non_english = True
|
|
61
|
+
self._translation_session = requests.Session()
|
|
60
62
|
|
|
61
63
|
def __enter__(self):
|
|
62
64
|
"""Enters the runtime context related to this object."""
|
|
@@ -70,6 +72,7 @@ class KKBox:
|
|
|
70
72
|
"""Closes the current session."""
|
|
71
73
|
if not self.is_session_closed:
|
|
72
74
|
self._session.close()
|
|
75
|
+
self._translation_session.close()
|
|
73
76
|
self._is_session_closed = True
|
|
74
77
|
|
|
75
78
|
@property
|
|
@@ -134,7 +137,12 @@ class KKBox:
|
|
|
134
137
|
self.__start_time = time.time()
|
|
135
138
|
|
|
136
139
|
def search(
|
|
137
|
-
self,
|
|
140
|
+
self,
|
|
141
|
+
artist: str,
|
|
142
|
+
song: str,
|
|
143
|
+
territory: str = "TW",
|
|
144
|
+
limit: int = 10,
|
|
145
|
+
normalize_non_english: bool = True,
|
|
138
146
|
) -> Optional[MusicInfo]:
|
|
139
147
|
"""
|
|
140
148
|
Searches for a song by artist and title.
|
|
@@ -149,8 +157,9 @@ class KKBox:
|
|
|
149
157
|
Two-letter country codes from ISO 3166-1 alpha-2.
|
|
150
158
|
Allowed values: ``HK``, ``JP``, ``MY``, ``SG``, ``TW``.
|
|
151
159
|
limit: int, optional
|
|
152
|
-
The number of items to retrieve from API.
|
|
153
|
-
|
|
160
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
|
|
161
|
+
normalize_non_english : bool, optional
|
|
162
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
154
163
|
|
|
155
164
|
Returns
|
|
156
165
|
-------
|
|
@@ -162,9 +171,13 @@ class KKBox:
|
|
|
162
171
|
"Artist and song names must be valid strings and can't be empty."
|
|
163
172
|
)
|
|
164
173
|
|
|
174
|
+
self.normalize_non_english = normalize_non_english
|
|
175
|
+
|
|
165
176
|
self.__refresh_token_if_expired()
|
|
166
177
|
|
|
167
|
-
query =
|
|
178
|
+
query = (
|
|
179
|
+
f"?q={artist} - {song}&type=track,album&territory={territory}&limit={limit}"
|
|
180
|
+
)
|
|
168
181
|
query_url = f"{self.api_url}/search{query}"
|
|
169
182
|
|
|
170
183
|
try:
|
|
@@ -288,12 +301,24 @@ class KKBox:
|
|
|
288
301
|
Optional[MusicInfo]
|
|
289
302
|
The music information if found, otherwise None.
|
|
290
303
|
"""
|
|
291
|
-
if not are_strings_similar(
|
|
304
|
+
if not are_strings_similar(
|
|
305
|
+
track["name"],
|
|
306
|
+
song,
|
|
307
|
+
use_translation=self.normalize_non_english,
|
|
308
|
+
translation_session=self._translation_session,
|
|
309
|
+
):
|
|
292
310
|
return None
|
|
293
311
|
|
|
294
312
|
artists_name = track["album"]["artist"]["name"]
|
|
295
313
|
matching_artists = (
|
|
296
|
-
artists_name
|
|
314
|
+
artists_name
|
|
315
|
+
if are_strings_similar(
|
|
316
|
+
artists_name,
|
|
317
|
+
artist,
|
|
318
|
+
use_translation=self.normalize_non_english,
|
|
319
|
+
translation_session=self._translation_session,
|
|
320
|
+
)
|
|
321
|
+
else None
|
|
297
322
|
)
|
|
298
323
|
|
|
299
324
|
if matching_artists:
|
|
@@ -336,12 +361,24 @@ class KKBox:
|
|
|
336
361
|
Optional[MusicInfo]
|
|
337
362
|
The music information if found, otherwise None.
|
|
338
363
|
"""
|
|
339
|
-
if not are_strings_similar(
|
|
364
|
+
if not are_strings_similar(
|
|
365
|
+
album["name"],
|
|
366
|
+
song,
|
|
367
|
+
use_translation=self.normalize_non_english,
|
|
368
|
+
translation_session=self._translation_session,
|
|
369
|
+
):
|
|
340
370
|
return None
|
|
341
371
|
|
|
342
372
|
artists_name = album["artist"]["name"]
|
|
343
373
|
matching_artists = (
|
|
344
|
-
artists_name
|
|
374
|
+
artists_name
|
|
375
|
+
if are_strings_similar(
|
|
376
|
+
artists_name,
|
|
377
|
+
artist,
|
|
378
|
+
use_translation=self.normalize_non_english,
|
|
379
|
+
translation_session=self._translation_session,
|
|
380
|
+
)
|
|
381
|
+
else None
|
|
345
382
|
)
|
|
346
383
|
|
|
347
384
|
if matching_artists:
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
from pprint import pprint
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
import requests
|
|
5
6
|
from ytmusicapi import YTMusic, exceptions
|
|
6
7
|
|
|
7
8
|
from yutipy.exceptions import (
|
|
@@ -19,8 +20,28 @@ class MusicYT:
|
|
|
19
20
|
def __init__(self) -> None:
|
|
20
21
|
"""Initializes the YouTube Music class and sets up the session."""
|
|
21
22
|
self.ytmusic = YTMusic()
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
self._is_session_closed = False
|
|
24
|
+
self.normalize_non_english = True
|
|
25
|
+
self._translation_session = requests.Session()
|
|
26
|
+
|
|
27
|
+
def close_session(self) -> None:
|
|
28
|
+
"""Closes the current session(s)."""
|
|
29
|
+
if not self.is_session_closed:
|
|
30
|
+
self._translation_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 search(
|
|
39
|
+
self,
|
|
40
|
+
artist: str,
|
|
41
|
+
song: str,
|
|
42
|
+
limit: int = 10,
|
|
43
|
+
normalize_non_english: bool = True,
|
|
44
|
+
) -> Optional[MusicInfo]:
|
|
24
45
|
"""
|
|
25
46
|
Searches for a song by artist and title.
|
|
26
47
|
|
|
@@ -31,8 +52,9 @@ class MusicYT:
|
|
|
31
52
|
song : str
|
|
32
53
|
The title of the song.
|
|
33
54
|
limit: int, optional
|
|
34
|
-
The number of items to retrieve from API.
|
|
35
|
-
|
|
55
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
|
|
56
|
+
normalize_non_english : bool, optional
|
|
57
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
36
58
|
|
|
37
59
|
Returns
|
|
38
60
|
-------
|
|
@@ -44,6 +66,8 @@ class MusicYT:
|
|
|
44
66
|
"Artist and song names must be valid strings and can't be empty."
|
|
45
67
|
)
|
|
46
68
|
|
|
69
|
+
self.normalize_non_english = normalize_non_english
|
|
70
|
+
|
|
47
71
|
query = f"{artist} - {song}"
|
|
48
72
|
|
|
49
73
|
try:
|
|
@@ -79,8 +103,18 @@ class MusicYT:
|
|
|
79
103
|
return False
|
|
80
104
|
|
|
81
105
|
return any(
|
|
82
|
-
are_strings_similar(
|
|
83
|
-
|
|
106
|
+
are_strings_similar(
|
|
107
|
+
result.get("title"),
|
|
108
|
+
song,
|
|
109
|
+
use_translation=self.normalize_non_english,
|
|
110
|
+
translation_session=self._translation_session,
|
|
111
|
+
)
|
|
112
|
+
and are_strings_similar(
|
|
113
|
+
_artist.get("name"),
|
|
114
|
+
artist,
|
|
115
|
+
use_translation=self.normalize_non_english,
|
|
116
|
+
translation_session=self._translation_session,
|
|
117
|
+
)
|
|
84
118
|
for _artist in result.get("artists", [])
|
|
85
119
|
)
|
|
86
120
|
|
|
@@ -16,6 +16,7 @@ from yutipy.exceptions import (
|
|
|
16
16
|
)
|
|
17
17
|
from yutipy.models import MusicInfo
|
|
18
18
|
from yutipy.utils.cheap_utils import (
|
|
19
|
+
guess_album_type,
|
|
19
20
|
are_strings_similar,
|
|
20
21
|
is_valid_string,
|
|
21
22
|
separate_artists,
|
|
@@ -62,6 +63,8 @@ class Spotify:
|
|
|
62
63
|
self.__header, self.__expires_in = self.__authenticate()
|
|
63
64
|
self.__start_time = time.time()
|
|
64
65
|
self._is_session_closed = False
|
|
66
|
+
self.normalize_non_english = True
|
|
67
|
+
self._translation_session = requests.Session()
|
|
65
68
|
|
|
66
69
|
def __enter__(self):
|
|
67
70
|
"""Enters the runtime context related to this object."""
|
|
@@ -72,9 +75,10 @@ class Spotify:
|
|
|
72
75
|
self.close_session()
|
|
73
76
|
|
|
74
77
|
def close_session(self) -> None:
|
|
75
|
-
"""Closes the current session."""
|
|
78
|
+
"""Closes the current session(s)."""
|
|
76
79
|
if not self.is_session_closed:
|
|
77
80
|
self._session.close()
|
|
81
|
+
self._translation_session.close()
|
|
78
82
|
self._is_session_closed = True
|
|
79
83
|
|
|
80
84
|
@property
|
|
@@ -138,7 +142,13 @@ class Spotify:
|
|
|
138
142
|
self.__header, self.__expires_in = self.__authenticate()
|
|
139
143
|
self.__start_time = time.time()
|
|
140
144
|
|
|
141
|
-
def search(
|
|
145
|
+
def search(
|
|
146
|
+
self,
|
|
147
|
+
artist: str,
|
|
148
|
+
song: str,
|
|
149
|
+
limit: int = 10,
|
|
150
|
+
normalize_non_english: bool = True,
|
|
151
|
+
) -> Optional[MusicInfo]:
|
|
142
152
|
"""
|
|
143
153
|
Searches for a song by artist and title.
|
|
144
154
|
|
|
@@ -149,8 +159,9 @@ class Spotify:
|
|
|
149
159
|
song : str
|
|
150
160
|
The title of the song.
|
|
151
161
|
limit: int, optional
|
|
152
|
-
The number of items to retrieve from API.
|
|
153
|
-
|
|
162
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``10``.
|
|
163
|
+
normalize_non_english : bool, optional
|
|
164
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
154
165
|
|
|
155
166
|
Returns
|
|
156
167
|
-------
|
|
@@ -162,7 +173,10 @@ class Spotify:
|
|
|
162
173
|
"Artist and song names must be valid strings and can't be empty."
|
|
163
174
|
)
|
|
164
175
|
|
|
176
|
+
self.normalize_non_english = normalize_non_english
|
|
177
|
+
|
|
165
178
|
music_info = None
|
|
179
|
+
artist_ids = None
|
|
166
180
|
queries = [
|
|
167
181
|
f"?q=artist:{artist} track:{song}&type=track&limit={limit}",
|
|
168
182
|
f"?q=artist:{artist} album:{song}&type=album&limit={limit}",
|
|
@@ -187,7 +201,7 @@ class Spotify:
|
|
|
187
201
|
if response.status_code != 200:
|
|
188
202
|
raise SpotifyException(f"Failed to search for music: {response.json()}")
|
|
189
203
|
|
|
190
|
-
artist_ids = self._get_artists_ids(artist)
|
|
204
|
+
artist_ids = artist_ids if artist_ids else self._get_artists_ids(artist)
|
|
191
205
|
music_info = self._find_music_info(
|
|
192
206
|
artist, song, response.json(), artist_ids
|
|
193
207
|
)
|
|
@@ -195,7 +209,13 @@ class Spotify:
|
|
|
195
209
|
return music_info
|
|
196
210
|
|
|
197
211
|
def search_advanced(
|
|
198
|
-
self,
|
|
212
|
+
self,
|
|
213
|
+
artist: str,
|
|
214
|
+
song: str,
|
|
215
|
+
isrc: str = None,
|
|
216
|
+
upc: str = None,
|
|
217
|
+
limit: int = 1,
|
|
218
|
+
normalize_non_english: bool = True,
|
|
199
219
|
) -> Optional[MusicInfo]:
|
|
200
220
|
"""
|
|
201
221
|
Searches for a song by artist, title, ISRC, or UPC.
|
|
@@ -210,6 +230,10 @@ class Spotify:
|
|
|
210
230
|
The ISRC of the track.
|
|
211
231
|
upc : str, optional
|
|
212
232
|
The UPC of the album.
|
|
233
|
+
limit: int, optional
|
|
234
|
+
The number of items to retrieve from API. ``limit >=1 and <= 50``. Default is ``1``.
|
|
235
|
+
normalize_non_english : bool, optional
|
|
236
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
213
237
|
|
|
214
238
|
Returns
|
|
215
239
|
-------
|
|
@@ -221,12 +245,14 @@ class Spotify:
|
|
|
221
245
|
"Artist and song names must be valid strings and can't be empty."
|
|
222
246
|
)
|
|
223
247
|
|
|
248
|
+
self.normalize_non_english = normalize_non_english
|
|
249
|
+
|
|
224
250
|
self.__refresh_token_if_expired()
|
|
225
251
|
|
|
226
252
|
if isrc:
|
|
227
|
-
query = f"?q={artist} {song} isrc:{isrc}&type=track&limit=
|
|
253
|
+
query = f"?q={artist} {song} isrc:{isrc}&type=track&limit={limit}"
|
|
228
254
|
elif upc:
|
|
229
|
-
query = f"?q={artist} {song} upc:{upc}&type=album&limit=
|
|
255
|
+
query = f"?q={artist} {song} upc:{upc}&type=album&limit={limit}"
|
|
230
256
|
else:
|
|
231
257
|
raise InvalidValueException("ISRC or UPC must be provided.")
|
|
232
258
|
|
|
@@ -340,14 +366,25 @@ class Spotify:
|
|
|
340
366
|
Optional[MusicInfo]
|
|
341
367
|
The music information if found, otherwise None.
|
|
342
368
|
"""
|
|
343
|
-
if not are_strings_similar(
|
|
369
|
+
if not are_strings_similar(
|
|
370
|
+
track["name"],
|
|
371
|
+
song,
|
|
372
|
+
use_translation=self.normalize_non_english,
|
|
373
|
+
translation_session=self._translation_session,
|
|
374
|
+
):
|
|
344
375
|
return None
|
|
345
376
|
|
|
346
377
|
artists_name = [x["name"] for x in track["artists"]]
|
|
347
378
|
matching_artists = [
|
|
348
379
|
x["name"]
|
|
349
380
|
for x in track["artists"]
|
|
350
|
-
if are_strings_similar(
|
|
381
|
+
if are_strings_similar(
|
|
382
|
+
x["name"],
|
|
383
|
+
artist,
|
|
384
|
+
use_translation=self.normalize_non_english,
|
|
385
|
+
translation_session=self._translation_session,
|
|
386
|
+
)
|
|
387
|
+
or x["id"] in artist_ids
|
|
351
388
|
]
|
|
352
389
|
|
|
353
390
|
if matching_artists:
|
|
@@ -392,21 +429,37 @@ class Spotify:
|
|
|
392
429
|
Optional[MusicInfo]
|
|
393
430
|
The music information if found, otherwise None.
|
|
394
431
|
"""
|
|
395
|
-
if not are_strings_similar(
|
|
432
|
+
if not are_strings_similar(
|
|
433
|
+
album["name"],
|
|
434
|
+
song,
|
|
435
|
+
use_translation=self.normalize_non_english,
|
|
436
|
+
translation_session=self._translation_session,
|
|
437
|
+
):
|
|
396
438
|
return None
|
|
397
439
|
|
|
398
440
|
artists_name = [x["name"] for x in album["artists"]]
|
|
399
441
|
matching_artists = [
|
|
400
442
|
x["name"]
|
|
401
443
|
for x in album["artists"]
|
|
402
|
-
if are_strings_similar(
|
|
444
|
+
if are_strings_similar(
|
|
445
|
+
x["name"],
|
|
446
|
+
artist,
|
|
447
|
+
use_translation=self.normalize_non_english,
|
|
448
|
+
translation_session=self._translation_session,
|
|
449
|
+
)
|
|
450
|
+
or x["id"] in artist_ids
|
|
403
451
|
]
|
|
404
452
|
|
|
405
453
|
if matching_artists:
|
|
454
|
+
guess = guess_album_type(album.get("total_tracks", 1))
|
|
455
|
+
guessed_right = are_strings_similar(
|
|
456
|
+
album.get("album_type", "x"), guess, use_translation=False
|
|
457
|
+
)
|
|
458
|
+
|
|
406
459
|
return MusicInfo(
|
|
407
460
|
album_art=album["images"][0]["url"],
|
|
408
461
|
album_title=album["name"],
|
|
409
|
-
album_type=album
|
|
462
|
+
album_type=album.get("alnum_type") if guessed_right else guess,
|
|
410
463
|
artists=", ".join(artists_name),
|
|
411
464
|
genre=None,
|
|
412
465
|
id=album["id"],
|
|
@@ -415,7 +468,7 @@ class Spotify:
|
|
|
415
468
|
release_date=album["release_date"],
|
|
416
469
|
tempo=None,
|
|
417
470
|
title=album["name"],
|
|
418
|
-
type=
|
|
471
|
+
type=album.get("type"),
|
|
419
472
|
upc=None,
|
|
420
473
|
url=album["external_urls"]["spotify"],
|
|
421
474
|
)
|
|
@@ -7,6 +7,7 @@ def translate_text(
|
|
|
7
7
|
text: str,
|
|
8
8
|
sl: str = None,
|
|
9
9
|
dl: str = "en",
|
|
10
|
+
session: requests.Session = None,
|
|
10
11
|
) -> dict:
|
|
11
12
|
"""
|
|
12
13
|
Translate text from one language to another.
|
|
@@ -15,7 +16,8 @@ def translate_text(
|
|
|
15
16
|
text (str): The text to be translated.
|
|
16
17
|
sl (str, optional): The source language code (e.g., 'en' for English, 'es' for Spanish). If not provided, the API will attempt to detect the source language.
|
|
17
18
|
dl (str, optional): The destination language code (default is 'en' for English).
|
|
18
|
-
|
|
19
|
+
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.
|
|
20
|
+
Providing your own session can improve performance by reusing the same session for multiple requests. Don't forget to close the session afterwards.
|
|
19
21
|
|
|
20
22
|
Returns:
|
|
21
23
|
dict: A dictionary containing the following keys:
|
|
@@ -24,12 +26,17 @@ def translate_text(
|
|
|
24
26
|
- 'destination-text': The translated text.
|
|
25
27
|
- 'destination-language': The destination language code.
|
|
26
28
|
"""
|
|
29
|
+
default_session = False
|
|
30
|
+
if session is None:
|
|
31
|
+
default_session = True
|
|
32
|
+
session = requests.Session()
|
|
33
|
+
|
|
27
34
|
if sl:
|
|
28
35
|
url = f"https://ftapi.pythonanywhere.com/translate?sl={sl}&dl={dl}&text={text}"
|
|
29
36
|
else:
|
|
30
37
|
url = f"https://ftapi.pythonanywhere.com/translate?dl={dl}&text={text}"
|
|
31
38
|
|
|
32
|
-
response =
|
|
39
|
+
response = session.get(url)
|
|
33
40
|
response_json = response.json()
|
|
34
41
|
result = {
|
|
35
42
|
"source-text": response_json["source-text"],
|
|
@@ -37,10 +44,20 @@ def translate_text(
|
|
|
37
44
|
"destination-text": response_json["destination-text"],
|
|
38
45
|
"destination-language": response_json["destination-language"],
|
|
39
46
|
}
|
|
47
|
+
|
|
48
|
+
if default_session:
|
|
49
|
+
session.close()
|
|
50
|
+
|
|
40
51
|
return result
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
def are_strings_similar(
|
|
54
|
+
def are_strings_similar(
|
|
55
|
+
str1: str,
|
|
56
|
+
str2: str,
|
|
57
|
+
threshold: int = 80,
|
|
58
|
+
use_translation: bool = True,
|
|
59
|
+
translation_session: requests.Session = None,
|
|
60
|
+
) -> bool:
|
|
44
61
|
"""
|
|
45
62
|
Determine if two strings are similar based on a given threshold.
|
|
46
63
|
|
|
@@ -48,12 +65,24 @@ def are_strings_similar(str1: str, str2: str, threshold: int = 80) -> bool:
|
|
|
48
65
|
str1 (str): First string to compare.
|
|
49
66
|
str2 (str): Second string to compare.
|
|
50
67
|
threshold (int, optional): Similarity threshold. Defaults to 80.
|
|
68
|
+
use_translation (bool, optional): Use translations to compare strings. Defaults to ``True``
|
|
69
|
+
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.
|
|
70
|
+
Providing your own session can improve performance by reusing the same session for multiple requests. Don't forget to close the session afterwards.
|
|
51
71
|
|
|
52
72
|
Returns:
|
|
53
73
|
bool: True if the strings are similar, otherwise False.
|
|
54
74
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
if use_translation:
|
|
76
|
+
str1 = (
|
|
77
|
+
translate_text(str1, session=translation_session)["destination-text"]
|
|
78
|
+
if translation_session
|
|
79
|
+
else translate_text(str1)["destination-text"]
|
|
80
|
+
)
|
|
81
|
+
str2 = (
|
|
82
|
+
translate_text(str2, session=translation_session)["destination-text"]
|
|
83
|
+
if translation_session
|
|
84
|
+
else translate_text(str2)["destination-text"]
|
|
85
|
+
)
|
|
57
86
|
|
|
58
87
|
similarity_score = fuzz.WRatio(str1, str2, processor=default_process)
|
|
59
88
|
return similarity_score > threshold
|
|
@@ -88,5 +117,15 @@ def is_valid_string(string: str) -> bool:
|
|
|
88
117
|
return bool(string and (string.isalnum() or not string.isspace()))
|
|
89
118
|
|
|
90
119
|
|
|
120
|
+
def guess_album_type(total_tracks: int):
|
|
121
|
+
"""Just guessing the album type (i.e. single, ep or album) by total track counts."""
|
|
122
|
+
if total_tracks == 1:
|
|
123
|
+
return "single"
|
|
124
|
+
if 3 <= total_tracks <= 5:
|
|
125
|
+
return "ep"
|
|
126
|
+
if total_tracks >= 7:
|
|
127
|
+
return "album"
|
|
128
|
+
|
|
129
|
+
|
|
91
130
|
if __name__ == "__main__":
|
|
92
131
|
separate_artists("Artist A ft. Artist B")
|
|
@@ -2,14 +2,17 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
2
2
|
from pprint import pprint
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
import requests
|
|
6
|
+
|
|
5
7
|
from yutipy.deezer import Deezer
|
|
6
|
-
from yutipy.exceptions import InvalidValueException
|
|
8
|
+
from yutipy.exceptions import InvalidValueException, KKBoxException, SpotifyException
|
|
7
9
|
from yutipy.itunes import Itunes
|
|
8
10
|
from yutipy.kkbox import KKBox
|
|
9
11
|
from yutipy.models import MusicInfo, MusicInfos
|
|
10
12
|
from yutipy.musicyt import MusicYT
|
|
11
13
|
from yutipy.spotify import Spotify
|
|
12
14
|
from yutipy.utils.cheap_utils import is_valid_string
|
|
15
|
+
from yutipy.utils.logger import logger
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class YutipyMusic:
|
|
@@ -22,22 +25,53 @@ class YutipyMusic:
|
|
|
22
25
|
def __init__(self) -> None:
|
|
23
26
|
"""Initializes the YutipyMusic class."""
|
|
24
27
|
self.music_info = MusicInfos()
|
|
25
|
-
self.album_art_priority = ["deezer", "
|
|
28
|
+
self.album_art_priority = ["deezer", "ytmusic", "itunes"]
|
|
26
29
|
self.services = {
|
|
27
30
|
"deezer": Deezer(),
|
|
28
31
|
"itunes": Itunes(),
|
|
29
|
-
"kkbox": KKBox(),
|
|
30
32
|
"ytmusic": MusicYT(),
|
|
31
|
-
"spotify": Spotify(),
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
try:
|
|
36
|
+
self.services["kkbox"] = KKBox()
|
|
37
|
+
except KKBoxException as e:
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"{self.__class__.__name__}: Skipping KKBox due to KKBoxException: {e}"
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
idx = self.album_art_priority.index("ytmusic")
|
|
43
|
+
self.album_art_priority.insert(idx, "kkbox")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self.services["spotify"] = Spotify()
|
|
47
|
+
except SpotifyException as e:
|
|
48
|
+
logger.warning(
|
|
49
|
+
f"{self.__class__.__name__}: Skipping Spotify due to SpotifyException: {e}"
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
idx = self.album_art_priority.index("ytmusic")
|
|
53
|
+
self.album_art_priority.insert(idx, "spotify")
|
|
54
|
+
self.normalize_non_english = True
|
|
55
|
+
self._translation_session = requests.Session()
|
|
56
|
+
|
|
57
|
+
# Assign the translation session to each service
|
|
58
|
+
for service in self.services.values():
|
|
59
|
+
if hasattr(service, "_translation_session"):
|
|
60
|
+
service._translation_session = self._translation_session
|
|
61
|
+
|
|
34
62
|
def __enter__(self) -> "YutipyMusic":
|
|
35
63
|
return self
|
|
36
64
|
|
|
37
65
|
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
|
38
66
|
self.close_sessions()
|
|
39
67
|
|
|
40
|
-
def search(
|
|
68
|
+
def search(
|
|
69
|
+
self,
|
|
70
|
+
artist: str,
|
|
71
|
+
song: str,
|
|
72
|
+
limit: int = 5,
|
|
73
|
+
normalize_non_english: bool = True,
|
|
74
|
+
) -> Optional[MusicInfos]:
|
|
41
75
|
"""
|
|
42
76
|
Searches for a song by artist and title.
|
|
43
77
|
|
|
@@ -48,8 +82,9 @@ class YutipyMusic:
|
|
|
48
82
|
song : str
|
|
49
83
|
The title of the song.
|
|
50
84
|
limit: int, optional
|
|
51
|
-
The number of items to retrieve from all APIs.
|
|
52
|
-
|
|
85
|
+
The number of items to retrieve from all APIs. ``limit >=1 and <= 50``. Default is ``5``.
|
|
86
|
+
normalize_non_english : bool, optional
|
|
87
|
+
Whether to normalize non-English characters for comparison. Default is ``True``.
|
|
53
88
|
|
|
54
89
|
Returns
|
|
55
90
|
-------
|
|
@@ -61,10 +96,30 @@ class YutipyMusic:
|
|
|
61
96
|
"Artist and song names must be valid strings and can't be empty."
|
|
62
97
|
)
|
|
63
98
|
|
|
99
|
+
self.normalize_non_english = normalize_non_english
|
|
100
|
+
|
|
101
|
+
attributes = [
|
|
102
|
+
"album_title",
|
|
103
|
+
"album_type",
|
|
104
|
+
"artists",
|
|
105
|
+
"genre",
|
|
106
|
+
"isrc",
|
|
107
|
+
"lyrics",
|
|
108
|
+
"release_date",
|
|
109
|
+
"tempo",
|
|
110
|
+
"title",
|
|
111
|
+
"type",
|
|
112
|
+
"upc",
|
|
113
|
+
]
|
|
114
|
+
|
|
64
115
|
with ThreadPoolExecutor() as executor:
|
|
65
116
|
futures = {
|
|
66
117
|
executor.submit(
|
|
67
|
-
service.search,
|
|
118
|
+
service.search,
|
|
119
|
+
artist=artist,
|
|
120
|
+
song=song,
|
|
121
|
+
limit=limit,
|
|
122
|
+
normalize_non_english=self.normalize_non_english,
|
|
68
123
|
): name
|
|
69
124
|
for name, service in self.services.items()
|
|
70
125
|
}
|
|
@@ -72,14 +127,16 @@ class YutipyMusic:
|
|
|
72
127
|
for future in as_completed(futures):
|
|
73
128
|
service_name = futures[future]
|
|
74
129
|
result = future.result()
|
|
75
|
-
self._combine_results(result, service_name)
|
|
130
|
+
self._combine_results(result, service_name, attributes)
|
|
76
131
|
|
|
77
132
|
if len(self.music_info.url) == 0:
|
|
78
133
|
return None
|
|
79
134
|
|
|
80
135
|
return self.music_info
|
|
81
136
|
|
|
82
|
-
def _combine_results(
|
|
137
|
+
def _combine_results(
|
|
138
|
+
self, result: Optional[MusicInfo], service_name: str, attributes: list
|
|
139
|
+
) -> None:
|
|
83
140
|
"""
|
|
84
141
|
Combines the results from different services.
|
|
85
142
|
|
|
@@ -93,25 +150,16 @@ class YutipyMusic:
|
|
|
93
150
|
if not result:
|
|
94
151
|
return
|
|
95
152
|
|
|
96
|
-
attributes = [
|
|
97
|
-
"album_title",
|
|
98
|
-
"album_type",
|
|
99
|
-
"artists",
|
|
100
|
-
"genre",
|
|
101
|
-
"isrc",
|
|
102
|
-
"lyrics",
|
|
103
|
-
"release_date",
|
|
104
|
-
"tempo",
|
|
105
|
-
"title",
|
|
106
|
-
"type",
|
|
107
|
-
"upc",
|
|
108
|
-
]
|
|
109
|
-
|
|
110
153
|
for attr in attributes:
|
|
111
154
|
if getattr(result, attr) and (
|
|
112
|
-
not getattr(self.music_info, attr)
|
|
155
|
+
not getattr(self.music_info, attr)
|
|
156
|
+
or (attr in ["genre", "album_type"] and service_name == "itunes")
|
|
113
157
|
):
|
|
114
|
-
setattr(
|
|
158
|
+
setattr(
|
|
159
|
+
self.music_info,
|
|
160
|
+
attr,
|
|
161
|
+
getattr(result, attributes.pop(attributes.index(attr))),
|
|
162
|
+
)
|
|
115
163
|
|
|
116
164
|
if result.album_art:
|
|
117
165
|
current_priority = self.album_art_priority.index(service_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yutipy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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>
|
|
@@ -33,7 +33,7 @@ Project-URL: Repository, https://github.com/CheapNightbot/yutipy.git
|
|
|
33
33
|
Project-URL: Issues, https://github.com/CheapNightbot/yutipy/issues
|
|
34
34
|
Project-URL: Changelog, https://github.com/CheapNightbot/yutipy/blob/master/CHANGELOG.md
|
|
35
35
|
Project-URL: funding, https://ko-fi.com/cheapnightbot
|
|
36
|
-
Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify
|
|
36
|
+
Keywords: music,API,Deezer,iTunes,Spotify,YouTube Music,search,retrieve,information,yutify,KKBox
|
|
37
37
|
Classifier: Development Status :: 4 - Beta
|
|
38
38
|
Classifier: Intended Audience :: Developers
|
|
39
39
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -32,6 +32,7 @@ tests/test_models.py
|
|
|
32
32
|
tests/test_musicyt.py
|
|
33
33
|
tests/test_spotify.py
|
|
34
34
|
tests/test_utils.py
|
|
35
|
+
tests/test_yutipy_music.py
|
|
35
36
|
yutipy/__init__.py
|
|
36
37
|
yutipy/deezer.py
|
|
37
38
|
yutipy/exceptions.py
|
|
@@ -47,4 +48,5 @@ yutipy.egg-info/dependency_links.txt
|
|
|
47
48
|
yutipy.egg-info/requires.txt
|
|
48
49
|
yutipy.egg-info/top_level.txt
|
|
49
50
|
yutipy/utils/__init__.py
|
|
50
|
-
yutipy/utils/cheap_utils.py
|
|
51
|
+
yutipy/utils/cheap_utils.py
|
|
52
|
+
yutipy/utils/logger.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
|