spots-cli 0.2.1__tar.gz → 0.2.2__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.
- {spots_cli-0.2.1 → spots_cli-0.2.2}/PKG-INFO +1 -1
- {spots_cli-0.2.1 → spots_cli-0.2.2}/pyproject.toml +1 -1
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/domain_resolver.py +9 -6
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/media_resolver.py +3 -1
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/youtube_extractor.py +12 -7
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/app/test_media_resolver.py +15 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/.gitignore +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/LICENSE +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/README.md +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/downloader.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/spotify_playlist_compilation.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/youtube_user_playlist.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/bootstrap/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/bootstrap/container.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/cli.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/deezer.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/secrets_manager.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/spotify.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/youtube.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/ytdl.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/config/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/history_manager.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/lyrics_finder.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/pattern_matcher.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/video_converter.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/web_scraper.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/file_storage.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/persistence_model.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/retry.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/integrations/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/integrations/spotify_user_playlist_modify.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/artist_metadata.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/errors.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/helper_models.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/media_resource.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/metadata.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/metadata_provider.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/playlist_info.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/search_provider.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/sentinel.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/unavailable_video.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/video_resolution.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/yt_video_info.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/add_to_history.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/bootstrap.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/duplicate_songs_remover.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/deezer_metadata_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/deezer_search_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/provider_search_container.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/providers_metadata.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/spotify_metadata_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/spotify_search_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/youtube_metadata_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/youtube_search_service.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/setup_env.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/__init__.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/config_dir.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/detect_browser.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/detect_js_runtimes.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/fetch.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/search_fallbacks.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/api/test_download_video.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/app/test_downloader.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_deezer.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_secrets_manager.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_spotify.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_youtube.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_ytdl.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/conftest.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/core/test_pattern_matcher.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/test_cli.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/test_spots.py +0 -0
- {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/utils/test_detect_js_runtimes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: spots_cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Spots is a modular music coordination library that unifies multiple providers (e.g. Deezer, Spotify, YouTube) into a single, consistent interface.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdesignscode/spots_flask
|
|
6
6
|
Project-URL: Issues, https://github.com/mdesignscode/spots_flask
|
|
@@ -8,6 +8,7 @@ from spots_cli.models import Metadata, SongNotFound, Sentinel, YTVideoInfo
|
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from spots_cli.bootstrap.container import Core
|
|
11
|
+
from spots_cli.core import YouTubeExtractor
|
|
11
12
|
from spots_cli.models import SearchProvider
|
|
12
13
|
from spots_cli.services import YoutubeSearchService
|
|
13
14
|
|
|
@@ -27,8 +28,9 @@ class ResolvedDomainResult:
|
|
|
27
28
|
youtube: YTVideoInfo
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def fallback_metadata(video: YTVideoInfo) -> Metadata:
|
|
31
|
-
"""Builds a bare-bones Metadata object straight from a YouTube result
|
|
31
|
+
def fallback_metadata(video: YTVideoInfo, extractor: YouTubeExtractor) -> Metadata:
|
|
32
|
+
"""Builds a bare-bones Metadata object straight from a YouTube result,
|
|
33
|
+
with the artist and title cleaned of video noise like "(Official Video)".
|
|
32
34
|
|
|
33
35
|
Used when no provider (Spotify/Deezer) has metadata for a track, or the
|
|
34
36
|
provider's result doesn't match. The provider is only used to prettify
|
|
@@ -36,9 +38,10 @@ def fallback_metadata(video: YTVideoInfo) -> Metadata:
|
|
|
36
38
|
from YouTube, so a missing/mismatched provider entry should never cause
|
|
37
39
|
a track to be dropped as long as it exists on YouTube.
|
|
38
40
|
"""
|
|
41
|
+
cleaned = extractor.clean_artist_and_title(video.uploader, video.title)
|
|
39
42
|
return Metadata(
|
|
40
|
-
title=video.title,
|
|
41
|
-
artist=video.uploader,
|
|
43
|
+
title=cleaned.title or video.title,
|
|
44
|
+
artist=cleaned.artist or video.uploader,
|
|
42
45
|
link=video.video_link,
|
|
43
46
|
artist_id="",
|
|
44
47
|
is_fallback=True,
|
|
@@ -97,7 +100,7 @@ class DomainResolver:
|
|
|
97
100
|
f"No provider metadata for '{video_title}', "
|
|
98
101
|
"falling back to YouTube-derived metadata"
|
|
99
102
|
)
|
|
100
|
-
fallback = fallback_metadata(video)
|
|
103
|
+
fallback = fallback_metadata(video, self.core.extractor)
|
|
101
104
|
self.core.storage.new(
|
|
102
105
|
query=video_title, result=fallback, query_type="metadata"
|
|
103
106
|
)
|
|
@@ -118,7 +121,7 @@ class DomainResolver:
|
|
|
118
121
|
f"Provider metadata for '{video_title}' didn't match, "
|
|
119
122
|
"falling back to YouTube-derived metadata"
|
|
120
123
|
)
|
|
121
|
-
fallback = fallback_metadata(video)
|
|
124
|
+
fallback = fallback_metadata(video, self.core.extractor)
|
|
122
125
|
self.core.storage.new(
|
|
123
126
|
query=video_title, result=fallback, query_type="metadata"
|
|
124
127
|
)
|
|
@@ -211,7 +211,9 @@ class MediaResolver:
|
|
|
211
211
|
f"No matching provider metadata for '{yt_title}', "
|
|
212
212
|
"falling back to YouTube-derived metadata"
|
|
213
213
|
)
|
|
214
|
-
metadata = fallback_metadata(
|
|
214
|
+
metadata = fallback_metadata(
|
|
215
|
+
video_info.result, self.core.extractor
|
|
216
|
+
)
|
|
215
217
|
|
|
216
218
|
self.core.storage.new(
|
|
217
219
|
query=yt_title, result=metadata, query_type="metadata"
|
|
@@ -166,15 +166,15 @@ class YouTubeExtractor:
|
|
|
166
166
|
artist=youtube_artist.strip(), title=youtube_title.strip()
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
-
def
|
|
170
|
-
"""Cleans noisy YouTube uploader/title fields into a
|
|
171
|
-
'Artist - Title' string for provider lookups (Deezer/Spotify).
|
|
169
|
+
def clean_artist_and_title(self, uploader: str, title: str) -> ArtistAndTitle:
|
|
170
|
+
"""Cleans noisy YouTube uploader/title fields into an artist and a title.
|
|
172
171
|
|
|
173
|
-
Strips common suffixes like "(Official Video)" and
|
|
174
|
-
uploader name when it's already repeated at the
|
|
172
|
+
Strips common suffixes like "(Official Video)" and a trailing "VEVO",
|
|
173
|
+
and de-duplicates the uploader name when it's already repeated at the
|
|
174
|
+
start of the title:
|
|
175
175
|
|
|
176
176
|
uploader="Tinashe", title="Tinashe - Needs (Official Video)"
|
|
177
|
-
-> "Tinashe
|
|
177
|
+
-> artist="Tinashe", title="Needs"
|
|
178
178
|
"""
|
|
179
179
|
uploader = self.remove_odd_keywords((uploader or "").strip())
|
|
180
180
|
uploader = sub(r"vevo$", "", uploader, flags=IGNORECASE).strip()
|
|
@@ -192,4 +192,9 @@ class YouTubeExtractor:
|
|
|
192
192
|
if deduped_title:
|
|
193
193
|
title = deduped_title
|
|
194
194
|
|
|
195
|
-
return
|
|
195
|
+
return ArtistAndTitle(artist=uploader, title=title)
|
|
196
|
+
|
|
197
|
+
def build_search_query(self, uploader: str, title: str) -> str:
|
|
198
|
+
"""The cleaned 'Artist - Title' string for provider lookups (Deezer/Spotify)."""
|
|
199
|
+
cleaned = self.clean_artist_and_title(uploader, title)
|
|
200
|
+
return f"{cleaned.artist} - {cleaned.title}".strip(" -")
|
|
@@ -78,3 +78,18 @@ def test_spotify_link_needs_spotify_turned_on(upload):
|
|
|
78
78
|
|
|
79
79
|
# not retried, and the Spotify id never reaches another provider
|
|
80
80
|
resolver.domain.provider_metadata.get.assert_not_called()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_fallback_tags_drop_the_repeated_artist_and_video_tags():
|
|
84
|
+
upload = YTVideoInfo(
|
|
85
|
+
id="kSBQZH3dkZ8",
|
|
86
|
+
title="Tyla - THAT GIRL (Biuro Version) (Official Video)",
|
|
87
|
+
uploader="TylaVEVO",
|
|
88
|
+
audio_ext="webm",
|
|
89
|
+
filesize=4,
|
|
90
|
+
)
|
|
91
|
+
resolver = build_resolver(upload, provider_error=SongNotFound("Tyla - THAT GIRL"))
|
|
92
|
+
|
|
93
|
+
metadata = resolver.resolve(url=LINK).metadata
|
|
94
|
+
|
|
95
|
+
assert (metadata.artist, metadata.title) == ("Tyla", "THAT GIRL (Biuro Version)")
|
|
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
|
{spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/integrations/spotify_user_playlist_modify.py
RENAMED
|
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
|
|
File without changes
|