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.
Files changed (81) hide show
  1. {spots_cli-0.2.1 → spots_cli-0.2.2}/PKG-INFO +1 -1
  2. {spots_cli-0.2.1 → spots_cli-0.2.2}/pyproject.toml +1 -1
  3. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/domain_resolver.py +9 -6
  4. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/media_resolver.py +3 -1
  5. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/youtube_extractor.py +12 -7
  6. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/app/test_media_resolver.py +15 -0
  7. {spots_cli-0.2.1 → spots_cli-0.2.2}/.gitignore +0 -0
  8. {spots_cli-0.2.1 → spots_cli-0.2.2}/LICENSE +0 -0
  9. {spots_cli-0.2.1 → spots_cli-0.2.2}/README.md +0 -0
  10. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/__init__.py +0 -0
  11. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/__init__.py +0 -0
  12. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/downloader.py +0 -0
  13. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/spotify_playlist_compilation.py +0 -0
  14. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/app/youtube_user_playlist.py +0 -0
  15. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/bootstrap/__init__.py +0 -0
  16. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/bootstrap/container.py +0 -0
  17. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/cli.py +0 -0
  18. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/__init__.py +0 -0
  19. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/deezer.py +0 -0
  20. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/secrets_manager.py +0 -0
  21. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/spotify.py +0 -0
  22. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/youtube.py +0 -0
  23. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/clients/ytdl.py +0 -0
  24. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/config/__init__.py +0 -0
  25. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/__init__.py +0 -0
  26. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/history_manager.py +0 -0
  27. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/lyrics_finder.py +0 -0
  28. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/pattern_matcher.py +0 -0
  29. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/video_converter.py +0 -0
  30. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/core/web_scraper.py +0 -0
  31. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/__init__.py +0 -0
  32. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/file_storage.py +0 -0
  33. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/persistence_model.py +0 -0
  34. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/engine/retry.py +0 -0
  35. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/integrations/__init__.py +0 -0
  36. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/integrations/spotify_user_playlist_modify.py +0 -0
  37. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/__init__.py +0 -0
  38. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/artist_metadata.py +0 -0
  39. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/errors.py +0 -0
  40. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/helper_models.py +0 -0
  41. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/media_resource.py +0 -0
  42. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/metadata.py +0 -0
  43. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/metadata_provider.py +0 -0
  44. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/playlist_info.py +0 -0
  45. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/search_provider.py +0 -0
  46. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/sentinel.py +0 -0
  47. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/unavailable_video.py +0 -0
  48. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/video_resolution.py +0 -0
  49. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/models/yt_video_info.py +0 -0
  50. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/__init__.py +0 -0
  51. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/add_to_history.py +0 -0
  52. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/bootstrap.py +0 -0
  53. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/scripts/duplicate_songs_remover.py +0 -0
  54. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/__init__.py +0 -0
  55. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/deezer_metadata_service.py +0 -0
  56. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/deezer_search_service.py +0 -0
  57. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/provider_search_container.py +0 -0
  58. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/providers_metadata.py +0 -0
  59. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/spotify_metadata_service.py +0 -0
  60. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/spotify_search_service.py +0 -0
  61. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/youtube_metadata_service.py +0 -0
  62. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/services/youtube_search_service.py +0 -0
  63. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/setup_env.py +0 -0
  64. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/__init__.py +0 -0
  65. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/config_dir.py +0 -0
  66. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/detect_browser.py +0 -0
  67. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/detect_js_runtimes.py +0 -0
  68. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/fetch.py +0 -0
  69. {spots_cli-0.2.1 → spots_cli-0.2.2}/src/spots_cli/utils/search_fallbacks.py +0 -0
  70. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/api/test_download_video.py +0 -0
  71. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/app/test_downloader.py +0 -0
  72. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_deezer.py +0 -0
  73. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_secrets_manager.py +0 -0
  74. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_spotify.py +0 -0
  75. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_youtube.py +0 -0
  76. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/clients/test_ytdl.py +0 -0
  77. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/conftest.py +0 -0
  78. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/core/test_pattern_matcher.py +0 -0
  79. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/test_cli.py +0 -0
  80. {spots_cli-0.2.1 → spots_cli-0.2.2}/tests/test_spots.py +0 -0
  81. {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.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "spots_cli"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  authors = [
9
9
  { name="Marlon Baatjes", email="marlonbaatjes62@gmail.com" }
10
10
  ]
@@ -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(video_info.result)
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 build_search_query(self, uploader: str, title: str) -> str:
170
- """Cleans noisy YouTube uploader/title fields into a search-friendly
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 de-duplicates the
174
- uploader name when it's already repeated at the start of the title:
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 - Needs"
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 f"{uploader} - {title}".strip(" -")
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