spots-cli 0.1.0__py3-none-any.whl
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/__init__.py +125 -0
- spots_cli/app/__init__.py +5 -0
- spots_cli/app/domain_resolver.py +157 -0
- spots_cli/app/downloader.py +78 -0
- spots_cli/app/media_resolver.py +180 -0
- spots_cli/app/spotify_playlist_compilation.py +251 -0
- spots_cli/app/youtube_user_playlist.py +215 -0
- spots_cli/bootstrap/__init__.py +1 -0
- spots_cli/bootstrap/container.py +230 -0
- spots_cli/cli.py +42 -0
- spots_cli/clients/__init__.py +5 -0
- spots_cli/clients/deezer.py +80 -0
- spots_cli/clients/secrets_manager.py +25 -0
- spots_cli/clients/spotify.py +79 -0
- spots_cli/clients/youtube.py +66 -0
- spots_cli/clients/ytdl.py +72 -0
- spots_cli/config/__init__.py +0 -0
- spots_cli/core/__init__.py +6 -0
- spots_cli/core/history_manager.py +39 -0
- spots_cli/core/lyrics_finder.py +58 -0
- spots_cli/core/pattern_matcher.py +226 -0
- spots_cli/core/video_converter.py +146 -0
- spots_cli/core/web_scraper.py +82 -0
- spots_cli/core/youtube_extractor.py +162 -0
- spots_cli/engine/__init__.py +3 -0
- spots_cli/engine/file_storage.py +347 -0
- spots_cli/engine/persistence_model.py +3 -0
- spots_cli/engine/retry.py +34 -0
- spots_cli/integrations/__init__.py +1 -0
- spots_cli/integrations/spotify_user_playlist_modify.py +47 -0
- spots_cli/models/__init__.py +24 -0
- spots_cli/models/artist_metadata.py +23 -0
- spots_cli/models/errors.py +69 -0
- spots_cli/models/helper_models.py +25 -0
- spots_cli/models/media_resource.py +20 -0
- spots_cli/models/metadata.py +36 -0
- spots_cli/models/metadata_provider.py +52 -0
- spots_cli/models/playlist_info.py +27 -0
- spots_cli/models/search_provider.py +65 -0
- spots_cli/models/sentinel.py +4 -0
- spots_cli/models/yt_video_info.py +29 -0
- spots_cli/scripts/__init__.py +2 -0
- spots_cli/scripts/add_to_history.py +45 -0
- spots_cli/scripts/bootstrap.py +92 -0
- spots_cli/scripts/duplicate_songs_remover.py +54 -0
- spots_cli/services/__init__.py +8 -0
- spots_cli/services/deezer_metadata_service.py +108 -0
- spots_cli/services/deezer_search_service.py +132 -0
- spots_cli/services/provider_search_container.py +67 -0
- spots_cli/services/providers_metadata.py +22 -0
- spots_cli/services/spotify_metadata_service.py +151 -0
- spots_cli/services/spotify_search_service.py +228 -0
- spots_cli/services/youtube_metadata_service.py +73 -0
- spots_cli/services/youtube_search_service.py +211 -0
- spots_cli/setup_env.py +118 -0
- spots_cli/utils/__init__.py +4 -0
- spots_cli/utils/config_dir.py +17 -0
- spots_cli/utils/detect_browser.py +30 -0
- spots_cli/utils/fetch.py +73 -0
- spots_cli/utils/search_fallbacks.py +21 -0
- spots_cli-0.1.0.dist-info/METADATA +146 -0
- spots_cli-0.1.0.dist-info/RECORD +65 -0
- spots_cli-0.1.0.dist-info/WHEEL +4 -0
- spots_cli-0.1.0.dist-info/entry_points.txt +2 -0
- spots_cli-0.1.0.dist-info/licenses/LICENSE +675 -0
spots_cli/setup_env.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from os.path import exists
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from spots_cli.utils import get_config_path, create_config_dir
|
|
5
|
+
|
|
6
|
+
FILENAME = get_config_path() / ".env"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def save_env(variables: dict[str, str]):
|
|
10
|
+
file_data = ""
|
|
11
|
+
|
|
12
|
+
for k, v in variables.items():
|
|
13
|
+
file_data += f"{k}={v}\n"
|
|
14
|
+
|
|
15
|
+
with open(FILENAME, "w") as f:
|
|
16
|
+
f.write(file_data)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def required_input(prompt: str):
|
|
20
|
+
value = ""
|
|
21
|
+
|
|
22
|
+
while not value:
|
|
23
|
+
value = input(f"[REQUIRED] {prompt}: ").strip()
|
|
24
|
+
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def optional_input(prompt: str, default: str | None = None):
|
|
29
|
+
value = input(
|
|
30
|
+
f"[OPTIONAL] {prompt}" + (f" [{default}]" if default else "") + ": "
|
|
31
|
+
).strip()
|
|
32
|
+
return value or default
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
if exists(FILENAME):
|
|
37
|
+
setup_again = input(
|
|
38
|
+
"Environment variables already exist.\n"
|
|
39
|
+
"Would you like to reconfigure them? (press Enter to cancel): "
|
|
40
|
+
).strip()
|
|
41
|
+
|
|
42
|
+
if not setup_again:
|
|
43
|
+
print("Setup cancelled.\n")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print("\n=== Environment Configuration ===\n")
|
|
47
|
+
|
|
48
|
+
create_config_dir()
|
|
49
|
+
|
|
50
|
+
static_cover_name = "https://i.postimg.cc/VvZTvvMV/single-cover.jpg"
|
|
51
|
+
static_playlist_cover_name = "https://i.postimg.cc/m2XzZsnq/youtube-playlist.jpg"
|
|
52
|
+
flask_port = 5000
|
|
53
|
+
main_provider: Literal["deezer", "spotify"] = "deezer"
|
|
54
|
+
spotify_features_available = "False"
|
|
55
|
+
youtube_account_access = "False"
|
|
56
|
+
avatar_icon = "https://i.postimg.cc/HnpL9ZrZ/avatar.jpg"
|
|
57
|
+
environment_mode = "PRODUCTION"
|
|
58
|
+
|
|
59
|
+
variables_map = {
|
|
60
|
+
"flask_port": flask_port,
|
|
61
|
+
"static_cover_name": static_cover_name,
|
|
62
|
+
"static_playlist_cover_name": static_playlist_cover_name,
|
|
63
|
+
"spotify_features_available": spotify_features_available,
|
|
64
|
+
"youtube_account_access": youtube_account_access,
|
|
65
|
+
"main_provider": main_provider,
|
|
66
|
+
"avatar_icon": avatar_icon,
|
|
67
|
+
"environment_mode": environment_mode,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
print("---- Spotify Configuration ----")
|
|
71
|
+
has_spotify_membership = (
|
|
72
|
+
input(
|
|
73
|
+
"Do you have a paid Spotify API membership? (required for Spotify features) [y/N]: "
|
|
74
|
+
)
|
|
75
|
+
.strip()
|
|
76
|
+
.lower()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if has_spotify_membership == "y":
|
|
80
|
+
spotify_username = required_input("Spotify username")
|
|
81
|
+
spotify_client_id = required_input("Spotify client ID")
|
|
82
|
+
spotify_client_secret = required_input("Spotify client secret")
|
|
83
|
+
|
|
84
|
+
default_redirect = "https://open.spotify.com/?"
|
|
85
|
+
spotify_redirect_uri = optional_input("Spotify redirect URI", default_redirect)
|
|
86
|
+
|
|
87
|
+
variables_map["username"] = spotify_username
|
|
88
|
+
variables_map["SPOTIPY_CLIENT_ID"] = spotify_client_id
|
|
89
|
+
variables_map["SPOTIPY_CLIENT_SECRET"] = spotify_client_secret
|
|
90
|
+
variables_map["SPOTIPY_REDIRECT_URI"] = spotify_redirect_uri
|
|
91
|
+
|
|
92
|
+
variables_map["spotify_features_available"] = "True"
|
|
93
|
+
variables_map["main_provider"] = "spotify"
|
|
94
|
+
|
|
95
|
+
print("\n---- Genius (Lyrics) Configuration ----")
|
|
96
|
+
has_genius_key = (
|
|
97
|
+
input("Do you have a Genius API key? (optional, for lyrics) [y/N]: ")
|
|
98
|
+
.strip()
|
|
99
|
+
.lower()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if has_genius_key == "y":
|
|
103
|
+
genius_key = required_input("Genius API key")
|
|
104
|
+
variables_map["lyricsgenius_key"] = genius_key
|
|
105
|
+
|
|
106
|
+
print("\n---- YouTube Configuration ----")
|
|
107
|
+
use_youtube_features = (
|
|
108
|
+
input("Enable YouTube account features (likes, library access)? [y/N]: ")
|
|
109
|
+
.strip()
|
|
110
|
+
.lower()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if use_youtube_features == "y":
|
|
114
|
+
variables_map["youtube_account_access"] = "True"
|
|
115
|
+
|
|
116
|
+
save_env(variables_map)
|
|
117
|
+
|
|
118
|
+
print("\n✅ Environment variables have been successfully configured.\n")
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
from spots_cli.utils.config_dir import get_config_path, create_config_dir
|
|
2
|
+
from spots_cli.utils.detect_browser import detect_browser
|
|
3
|
+
from spots_cli.utils.fetch import fetch_data, FetchResponseFailure, FetchResponseSuccess
|
|
4
|
+
from spots_cli.utils.search_fallbacks import search_fallbacks
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_config_path() -> Path:
|
|
7
|
+
if sys.platform == "win32":
|
|
8
|
+
return Path(os.getenv("APPDATA", Path.home() / "AppData/Roaming")) / "spots_cli"
|
|
9
|
+
elif sys.platform == "darwin":
|
|
10
|
+
return Path.home() / "Library/Application Support" / "spots_cli"
|
|
11
|
+
else:
|
|
12
|
+
return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) / "spots_cli"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_config_dir():
|
|
16
|
+
config_dir = get_config_path()
|
|
17
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_browser() -> Optional[Tuple[str, ...]]:
|
|
8
|
+
"""
|
|
9
|
+
Detect an installed browser and return a yt-dlp compatible
|
|
10
|
+
cookiesfrombrowser tuple.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Tuple usable in yt-dlp options, e.g. ("firefox",)
|
|
14
|
+
or ("chrome",), or None if nothing found.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Order matters: prefer Firefox (more reliable on Linux)
|
|
18
|
+
browsers = [
|
|
19
|
+
("firefox", ["firefox"]),
|
|
20
|
+
("chrome", ["google-chrome", "chrome", "chromium", "chromium-browser"]),
|
|
21
|
+
("edge", ["microsoft-edge"]),
|
|
22
|
+
("brave", ["brave-browser"]),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
for browser_name, executables in browsers:
|
|
26
|
+
for exe in executables:
|
|
27
|
+
if shutil.which(exe):
|
|
28
|
+
return (browser_name,)
|
|
29
|
+
|
|
30
|
+
return None
|
spots_cli/utils/fetch.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from requests import get
|
|
3
|
+
from requests.exceptions import Timeout, ConnectionError, RequestException, HTTPError
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class FetchResponseFailure:
|
|
9
|
+
error: str
|
|
10
|
+
status_code: int = field(default=500)
|
|
11
|
+
success: Literal[False] = False
|
|
12
|
+
data: None = None
|
|
13
|
+
fallback: Any = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class FetchResponseSuccess:
|
|
18
|
+
data: dict[str, Any] | str
|
|
19
|
+
status_code: int = field(default=200)
|
|
20
|
+
success: Literal[True] = True
|
|
21
|
+
error: None = None
|
|
22
|
+
fallback: Any = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fetch_data(
|
|
26
|
+
url: str,
|
|
27
|
+
*,
|
|
28
|
+
params: dict[str, Any] | None = None,
|
|
29
|
+
headers: dict[str, Any] | None = None,
|
|
30
|
+
timeout: int = 5,
|
|
31
|
+
) -> FetchResponseFailure | FetchResponseSuccess:
|
|
32
|
+
try:
|
|
33
|
+
response = get(url, params=params, headers=headers, timeout=timeout)
|
|
34
|
+
|
|
35
|
+
# Check for HTTP errors (4xx, 5xx)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
|
|
38
|
+
# Try to parse JSON (optional, adjust if expecting text)
|
|
39
|
+
try:
|
|
40
|
+
res_data = response.json()
|
|
41
|
+
|
|
42
|
+
if res_data.get("error"):
|
|
43
|
+
error_data = res_data["error"]
|
|
44
|
+
return FetchResponseFailure(
|
|
45
|
+
status_code=error_data.get("code"), error=error_data["message"]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return FetchResponseSuccess(status_code=response.status_code, data=res_data)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return FetchResponseSuccess(
|
|
51
|
+
status_code=response.status_code,
|
|
52
|
+
data=response.text, # fallback if not JSON
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
except Timeout:
|
|
56
|
+
return FetchResponseFailure(
|
|
57
|
+
error="Request timed out",
|
|
58
|
+
fallback=None, # you can plug in cached data here
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
except ConnectionError:
|
|
62
|
+
return FetchResponseFailure(error="Connection error", fallback=None)
|
|
63
|
+
|
|
64
|
+
except HTTPError as e:
|
|
65
|
+
return FetchResponseFailure(
|
|
66
|
+
error=f"HTTP error: {e}",
|
|
67
|
+
status_code=getattr(e.response, "status_code", 500),
|
|
68
|
+
fallback=None,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
except RequestException as e:
|
|
72
|
+
# Catch-all for other request issues
|
|
73
|
+
return FetchResponseFailure(error=f"Unexpected error: {e}", fallback=None)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from spots_cli.models import SongNotFound
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from spots_cli.models import Metadata, SearchProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def search_fallbacks(*, query: str, providers: list[SearchProvider]) -> Metadata:
|
|
12
|
+
index = 0
|
|
13
|
+
while index < len(providers):
|
|
14
|
+
provider = providers[index]
|
|
15
|
+
try:
|
|
16
|
+
return provider.search_track(query)
|
|
17
|
+
except SongNotFound:
|
|
18
|
+
index += 1
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
raise SongNotFound(query)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spots_cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spots is a modular music coordination library that unifies multiple providers (e.g. Deezer, Spotify, YouTube) into a single, consistent interface.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mdesignscode/spots_flask
|
|
6
|
+
Project-URL: Issues, https://github.com/mdesignscode/spots_flask
|
|
7
|
+
Author-email: Marlon Baatjes <marlonbaatjes62@gmail.com>
|
|
8
|
+
License-Expression: GPL-3.0-or-later
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: beautifulsoup4
|
|
17
|
+
Requires-Dist: google-api-python-client
|
|
18
|
+
Requires-Dist: google-auth-oauthlib
|
|
19
|
+
Requires-Dist: lyricsgenius
|
|
20
|
+
Requires-Dist: moviepy
|
|
21
|
+
Requires-Dist: mutagen
|
|
22
|
+
Requires-Dist: python-dotenv
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Requires-Dist: spotipy
|
|
25
|
+
Requires-Dist: tenacity
|
|
26
|
+
Requires-Dist: typer
|
|
27
|
+
Requires-Dist: yt-dlp
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# 🎧 Spots
|
|
31
|
+
|
|
32
|
+
Spots is a modular music coordination library that unifies multiple providers (e.g. Deezer, Spotify, YouTube) into a single, consistent interface.
|
|
33
|
+
|
|
34
|
+
It resolves tracks across platforms, matches metadata intelligently, and enables downloading, playlist handling, and media conversion — all through a clean, configurable API.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ✨ Features
|
|
39
|
+
|
|
40
|
+
- 🔍 **Multi-provider search**
|
|
41
|
+
- Supports multiple metadata providers with fallback resolution
|
|
42
|
+
|
|
43
|
+
- 🧠 **Intelligent track matching**
|
|
44
|
+
- Matches tracks across platforms using pattern-based scoring
|
|
45
|
+
|
|
46
|
+
- 🎵 **Unified media resolution**
|
|
47
|
+
- Seamlessly map metadata → YouTube → downloadable media
|
|
48
|
+
|
|
49
|
+
- ⚡ **Provider fallback system**
|
|
50
|
+
- Automatically falls back when a provider fails
|
|
51
|
+
|
|
52
|
+
- 🗂️ **Built-in caching**
|
|
53
|
+
- Reduces redundant API calls and speeds up repeated queries
|
|
54
|
+
|
|
55
|
+
- ⚙️ **Config-driven architecture**
|
|
56
|
+
- Enable/disable integrations (Spotify, YouTube, etc.)
|
|
57
|
+
|
|
58
|
+
- 📦 **Download + conversion pipeline**
|
|
59
|
+
- Handles media extraction and conversion (via yt-dlp)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 📦 Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install spots
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ⚙️ Setup
|
|
72
|
+
|
|
73
|
+
### Run the setup script:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
spots setup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
* follow the prompts to generate the config folder
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🚀 Usage
|
|
85
|
+
```bash
|
|
86
|
+
# search a title
|
|
87
|
+
# required search format: `Artist - Title`
|
|
88
|
+
spots download "Kelsey Lynn - Modern Day Marilyn Monroe"
|
|
89
|
+
|
|
90
|
+
# download a direct url
|
|
91
|
+
spots download "https://www.youtube.com/watch?v=UmZPa5Qudtw"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Spots will:
|
|
95
|
+
|
|
96
|
+
* Resolve the track via metadata providers
|
|
97
|
+
* Match it to a YouTube video
|
|
98
|
+
* Download and convert it
|
|
99
|
+
* Store results locally
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🎮 Available commands
|
|
104
|
+
|
|
105
|
+
* download - downloads a direct url or a search query
|
|
106
|
+
```bash
|
|
107
|
+
spots download "Kelsey Lynn - Modern Day Marilyn Monroe"
|
|
108
|
+
```
|
|
109
|
+
* migrate-likes - transfers likes from Spotify to YouTube (more providers coming soon...)
|
|
110
|
+
```bash
|
|
111
|
+
spots migrate-likes
|
|
112
|
+
```
|
|
113
|
+
* add-to-history - adds a user's downloaded songs to the Spots history. Avoids duplicate downloads.
|
|
114
|
+
```bash
|
|
115
|
+
spots add-to-history path-to-music
|
|
116
|
+
```
|
|
117
|
+
* remove-duplicates - removes duplicate songs in a given folder.
|
|
118
|
+
```bash
|
|
119
|
+
spots remove-duplicates path-to-music
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## ⚠️ Important
|
|
125
|
+
|
|
126
|
+
The Spotify API content may not be downloaded using this project. This project is created for personal use only and is intended for educational purposes. Any use of this project to download or distribute copyrighted material without proper authorization is against the terms of service of Spotify and other involved platforms. The project's author and contributors are not responsible for any misuse of this software.
|
|
127
|
+
|
|
128
|
+
## 🤝 Contributing
|
|
129
|
+
|
|
130
|
+
Contributions are welcome.
|
|
131
|
+
|
|
132
|
+
If you're adding a provider or improving matching logic, open a PR with a clear description of changes.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 📄 License
|
|
137
|
+
|
|
138
|
+
GPL-3.0-or-later
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 💬 Summary
|
|
143
|
+
|
|
144
|
+
Spots is designed to make cross-platform music workflows simple:
|
|
145
|
+
|
|
146
|
+
Give it a track — it figures out the rest.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
spots_cli/__init__.py,sha256=zABCq2Zh8M51aFrieVzvlCaLB6ra3nwW29RtU2vCrog,4381
|
|
2
|
+
spots_cli/cli.py,sha256=j85SwJKe3w6E8uBJ7zVgAhd2afiKkeFJ-JLgzT9Wl0A,700
|
|
3
|
+
spots_cli/setup_env.py,sha256=ITcUDM4r2OFgro3D6rNds5PBEITjh4yAynGpbJ6GR9I,3647
|
|
4
|
+
spots_cli/app/__init__.py,sha256=gZwcWMaPOMY152WWvX6d4SOxKXxb1qTQEvaN9seXi1c,310
|
|
5
|
+
spots_cli/app/domain_resolver.py,sha256=V7EdvDQYepC7fO8bxQ3Kcm1aizGbyxKM7_kXitv9228,5709
|
|
6
|
+
spots_cli/app/downloader.py,sha256=CFtYsHJ1giGl9er1iqNK__fjumgn5pskji4hmm_pL9E,2651
|
|
7
|
+
spots_cli/app/media_resolver.py,sha256=AqXQ4h6eY9sNrUifG6kHi2ML1-4XWpM9tkQvs0OFLUQ,6635
|
|
8
|
+
spots_cli/app/spotify_playlist_compilation.py,sha256=Ud1lpsikoTEUw-hXsNmEqBqMJnMSSC18jAkyTT3gCzA,8514
|
|
9
|
+
spots_cli/app/youtube_user_playlist.py,sha256=Z6UNxvOG8h_gjNw1Oi4mprt2sG6A-0VI8IfYfguM8ek,7320
|
|
10
|
+
spots_cli/bootstrap/__init__.py,sha256=CXvBouMV7Yx9TZMWU4BpY2Ccw0PFh1X6sBeDauYLJhA,52
|
|
11
|
+
spots_cli/bootstrap/container.py,sha256=pTDnH56u5x1NDDZThADaZP8hwFaC8REj0YQHr3ENyCY,6989
|
|
12
|
+
spots_cli/clients/__init__.py,sha256=MM4DLhZoZXxqAVys3VDn9XGRC_1M4NY0Qq6yOQpcVB4,265
|
|
13
|
+
spots_cli/clients/deezer.py,sha256=EZ8z6tiHin2W3syDjh80fx7z95-kSVmviWLg1a31ZWQ,2866
|
|
14
|
+
spots_cli/clients/secrets_manager.py,sha256=zLBziF24mHvxN6rII2iMZHQbrNsYLQKs9vGPswP5Qiw,579
|
|
15
|
+
spots_cli/clients/spotify.py,sha256=JFyscB27sD7gIH5dzM1XVN1dl_6YQOxtTe5r_7ZfpVA,2178
|
|
16
|
+
spots_cli/clients/youtube.py,sha256=SKrUGj27DI9_rbYwj8rDzZtzS1z0jEGOgQDpj0bQGUU,1886
|
|
17
|
+
spots_cli/clients/ytdl.py,sha256=BvdHr9g90z16PrtQn7NP8YG9_TMIfhEMTT9VjSKaCPY,2097
|
|
18
|
+
spots_cli/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
spots_cli/core/__init__.py,sha256=JywasH_HkZlJyNsp3BZEYONcaYCrk_ud15yomxRpPUc,340
|
|
20
|
+
spots_cli/core/history_manager.py,sha256=qkHUZ-tLWCVYg9yyCyOmYHMArmdCihyLMpb-cCap4mA,1071
|
|
21
|
+
spots_cli/core/lyrics_finder.py,sha256=UFBbUjn8elNmbg8cy1O-CQNNzIbbNJwOG7BM_PtMLvw,1886
|
|
22
|
+
spots_cli/core/pattern_matcher.py,sha256=AjTdYd2mwIe-aUtNtq__CnLDxBDC1-SrQkZ5CDB8H4U,5943
|
|
23
|
+
spots_cli/core/video_converter.py,sha256=ynmdoFeVSUncab60WCiUFTEpOaOV5JsBXvSZwfLyKrw,4333
|
|
24
|
+
spots_cli/core/web_scraper.py,sha256=gPyYeazP9x1jS2B_KhNmNyKtd3izU2uhXgBNy8qrnIc,2782
|
|
25
|
+
spots_cli/core/youtube_extractor.py,sha256=kmLzRD14aFY0FjgmQ3Nl3ZRPWHnPorEjobT4zh7dhWk,6131
|
|
26
|
+
spots_cli/engine/__init__.py,sha256=egIAqQuK5X1ZH-X4beRwiO00BkfRpJmsZ5YUyS_UTqg,150
|
|
27
|
+
spots_cli/engine/file_storage.py,sha256=UDUmCJiHCHiPSPqXkYpPhemNyBSZ_Opdhfk_0oWwEJc,10567
|
|
28
|
+
spots_cli/engine/persistence_model.py,sha256=LOII__m_4cjHrxQ7Ncrw7-4e-T0b9uAZf2B4jFjoIU4,69
|
|
29
|
+
spots_cli/engine/retry.py,sha256=8iydX-FaapYi8zC6Fe4yb0YKyiXcn8W8LvMuAq5Ge7g,811
|
|
30
|
+
spots_cli/integrations/__init__.py,sha256=f9qlGiEZlUhO59Ed2PborXklmhEYjqmHNCSBfCd6hkU,90
|
|
31
|
+
spots_cli/integrations/spotify_user_playlist_modify.py,sha256=AxNeCrX5djfJx01PnPRk19uFC6jm2crT1GQ2ky6p7Cc,1520
|
|
32
|
+
spots_cli/models/__init__.py,sha256=5LHfh4tRk4h9guPFK7n9LhiS-jIV6eFYwb0izphFrwA,857
|
|
33
|
+
spots_cli/models/artist_metadata.py,sha256=JzuYFBUVnAf_PJXD69_WrTWmdWH0mQFKOWJVH6zMYM8,490
|
|
34
|
+
spots_cli/models/errors.py,sha256=XzLO-cGOHEGSc_OxgnQPX2WEl24L-OfkhZZlBl4lTD8,1737
|
|
35
|
+
spots_cli/models/helper_models.py,sha256=5y6vaQvbCH7DeXGVjaFUft2O4UYzqb4SO8_NlLZcl_k,411
|
|
36
|
+
spots_cli/models/media_resource.py,sha256=DgPlIHWaKmANL3vvglUAv6nBaw5jrVpPvXTxYgvgZC8,441
|
|
37
|
+
spots_cli/models/metadata.py,sha256=Gb55kH1SekX_RTN-KaQ3c3GIVDa0N7OGjNqXymLS4TQ,1300
|
|
38
|
+
spots_cli/models/metadata_provider.py,sha256=PJbxy9eV2B5CWkP1dzoSKXElHYNVKAPJm7GXNr0oEmg,1372
|
|
39
|
+
spots_cli/models/playlist_info.py,sha256=ntWoXYXclISoBdF5uFvc4Zg1GdzZzfrdBNt-qXrK1PU,762
|
|
40
|
+
spots_cli/models/search_provider.py,sha256=0WPr58HjW5abG-T_18eJNjQ5VE117P_-xpYhzPBCikY,1597
|
|
41
|
+
spots_cli/models/sentinel.py,sha256=IayHOg7yErybvgEkzV9eExpTg0szoks7O6T9HnWNbng,71
|
|
42
|
+
spots_cli/models/yt_video_info.py,sha256=COkhWuZTRlyqi0k_0TlR_kNhEhRHeySdt8jNf918S-g,647
|
|
43
|
+
spots_cli/scripts/__init__.py,sha256=sSiavjYeJ1rIA6Fp9B3gaoL-51Do85j97JarXdfj8rY,153
|
|
44
|
+
spots_cli/scripts/add_to_history.py,sha256=PW7uOh6FAa-mx4Adzs13Clt5AQFmQYIHkLyxDG4h5_g,1419
|
|
45
|
+
spots_cli/scripts/bootstrap.py,sha256=OHvtqMHoFy1Ztxe5K2jxZkikU3-ZV5-0xHM_bclZHz4,2609
|
|
46
|
+
spots_cli/scripts/duplicate_songs_remover.py,sha256=0kZWmp6pZT-Z4W7hUkcxZAWqC3tHW0tIdBc6iQcNGEs,1526
|
|
47
|
+
spots_cli/services/__init__.py,sha256=7M_Touz-BuV0ggEI1Zj67aJLe2WBaJJSvWYWELETRDw,607
|
|
48
|
+
spots_cli/services/deezer_metadata_service.py,sha256=sOxmxDCLC_zhC_8yiTTfk28futJcwyTGeEzswehuL-Y,3491
|
|
49
|
+
spots_cli/services/deezer_search_service.py,sha256=6v402oGtOuCc4Xq8wa3yTWypqui3oegB3psigdBX8-4,4167
|
|
50
|
+
spots_cli/services/provider_search_container.py,sha256=hNlVowitTrCNNp0VudIe1SSTxm0C8CrAkCXVrceHONc,2261
|
|
51
|
+
spots_cli/services/providers_metadata.py,sha256=vjTCGYTqNEeMm68HMh28rdz8POAQmdrV6D1Rn-PJh_k,736
|
|
52
|
+
spots_cli/services/spotify_metadata_service.py,sha256=UlVyLkNtBwDGjSyzPFNy68pk62uEIldEW9hiRiPO-cw,4681
|
|
53
|
+
spots_cli/services/spotify_search_service.py,sha256=Ayf3Tljuon_-BZ8ueO7FLPT2K3FD0HWLF7IFg2pzGrc,7312
|
|
54
|
+
spots_cli/services/youtube_metadata_service.py,sha256=Yti_9MViJhY5e4G5mKvXpsHBsOUEgh9cJUyaetSFQNg,2164
|
|
55
|
+
spots_cli/services/youtube_search_service.py,sha256=YEkDuls0m5mKEyl8XpnhfC1A10h810sVzG0wddKrCs4,7013
|
|
56
|
+
spots_cli/utils/__init__.py,sha256=UjTFuPmsWvnduES7r_Y-Mx0TmJn4KW-N_-x_xUTHS-M,283
|
|
57
|
+
spots_cli/utils/config_dir.py,sha256=H7TvwPTThXYHIBJ_jDB1jNR0ISeDTUIfrYGO553I2lQ,519
|
|
58
|
+
spots_cli/utils/detect_browser.py,sha256=qxs-a6WZCJaGq9d6OphASM817UzdUhuek8KPk3H4XmI,815
|
|
59
|
+
spots_cli/utils/fetch.py,sha256=usqFnMFuF0sMLTYSsageOvKIULagMTvlzxT4KhwCCd0,2200
|
|
60
|
+
spots_cli/utils/search_fallbacks.py,sha256=TH8Nfkomh-ad_e5sG8ccLhjfSx-MmBoRC0dr_xH8ONo,522
|
|
61
|
+
spots_cli-0.1.0.dist-info/METADATA,sha256=2eToqgcJYJu5-RPl_1r8-suRe7gii0pBGkkQTLBGISU,4028
|
|
62
|
+
spots_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
63
|
+
spots_cli-0.1.0.dist-info/entry_points.txt,sha256=Sqau0y_5RbvwCSLU7vpGnPLIMg5CUETmgEIk-sqPpvI,49
|
|
64
|
+
spots_cli-0.1.0.dist-info/licenses/LICENSE,sha256=EeUxX9Af6TyAbyaTWp-HaboYttd8zzXfXmvxM471Evc,35158
|
|
65
|
+
spots_cli-0.1.0.dist-info/RECORD,,
|