SpotDown 1.0.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spotdown-1.0.0/SpotDown.egg-info → spotdown-1.3.0}/PKG-INFO +28 -11
- {spotdown-1.0.0 → spotdown-1.3.0}/README.md +26 -9
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/downloader/youtube_downloader.py +3 -2
- spotdown-1.3.0/SpotDown/extractor/spotify_extractor.py +218 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/main.py +2 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/upload/version.py +1 -1
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/console_utils.py +1 -1
- spotdown-1.3.0/SpotDown/utils/ffmpeg_installer.py +374 -0
- spotdown-1.3.0/SpotDown/utils/file_utils.py +233 -0
- {spotdown-1.0.0 → spotdown-1.3.0/SpotDown.egg-info}/PKG-INFO +28 -11
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/SOURCES.txt +1 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/requires.txt +1 -1
- {spotdown-1.0.0 → spotdown-1.3.0}/requirements.txt +2 -2
- spotdown-1.0.0/SpotDown/extractor/spotify_extractor.py +0 -355
- spotdown-1.0.0/SpotDown/utils/file_utils.py +0 -129
- {spotdown-1.0.0 → spotdown-1.3.0}/LICENSE +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/MANIFEST.in +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/__init__.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/downloader/__init__.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/extractor/__init__.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/extractor/youtube_extractor.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/__init__.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/config_json.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/headers.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/logger.py +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/dependency_links.txt +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/entry_points.txt +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/top_level.txt +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/setup.cfg +0 -0
- {spotdown-1.0.0 → spotdown-1.3.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: SpotDown
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0
|
4
4
|
Summary: A command-line program to download music
|
5
5
|
Home-page: https://github.com/Arrowar/spotdown
|
6
6
|
Author: Arrowar
|
@@ -13,12 +13,12 @@ Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
14
14
|
Requires-Dist: rich
|
15
15
|
Requires-Dist: httpx
|
16
|
-
Requires-Dist: playwright
|
17
16
|
Requires-Dist: unidecode
|
18
17
|
Requires-Dist: ua-generator
|
19
18
|
Requires-Dist: unidecode
|
20
19
|
Requires-Dist: yt-dlp
|
21
20
|
Requires-Dist: Pillow
|
21
|
+
Requires-Dist: spotipy
|
22
22
|
Dynamic: author
|
23
23
|
Dynamic: author-email
|
24
24
|
Dynamic: classifier
|
@@ -42,6 +42,13 @@ Dynamic: summary
|
|
42
42
|
|
43
43
|
[](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
|
44
44
|
|
45
|
+
## 🚀 Download & Install
|
46
|
+
|
47
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_win.exe)
|
48
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_mac)
|
49
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_latest)
|
50
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_previous)
|
51
|
+
|
45
52
|
---
|
46
53
|
|
47
54
|
*⚡ **Quick Start:** `pip install spotdown && spotdown`*
|
@@ -52,9 +59,9 @@ Dynamic: summary
|
|
52
59
|
|
53
60
|
- [✨ Features](#features)
|
54
61
|
- [🛠️ Installation](#️installation)
|
62
|
+
- [⚙️ Setup](#setup)
|
55
63
|
- [⚙️ Configuration](#configuration)
|
56
64
|
- [💻 Usage](#usage)
|
57
|
-
- [⚠️ Disclaimer](#disclaimer)
|
58
65
|
|
59
66
|
## Features
|
60
67
|
|
@@ -99,6 +106,24 @@ After installation, run this one-time setup command:
|
|
99
106
|
playwright install chromium
|
100
107
|
```
|
101
108
|
|
109
|
+
## Setup
|
110
|
+
|
111
|
+
1. Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
|
112
|
+
2. Log in and create a new application
|
113
|
+
3. Copy your **Client ID** and **Client Secret**
|
114
|
+
4. Create a file named `.env` in the SpotDown directory with the following content:
|
115
|
+
|
116
|
+
```
|
117
|
+
SPOTIFY_CLIENT_ID=your_client_id_here
|
118
|
+
SPOTIPY_CLIENT_SECRET=your_client_secret_here
|
119
|
+
```
|
120
|
+
|
121
|
+
5. Save the file. SpotDown will automatically load these credentials.
|
122
|
+
|
123
|
+
### Error Handling
|
124
|
+
- If the credentials are missing, SpotDown will log an error and exit.
|
125
|
+
- If the credentials are invalid, SpotDown will log an error and exit. Please double-check your `.env` file and credentials.
|
126
|
+
|
102
127
|
## Configuration
|
103
128
|
|
104
129
|
SpotDown uses a JSON configuration file with the following structure:
|
@@ -112,10 +137,6 @@ SpotDown uses a JSON configuration file with the following structure:
|
|
112
137
|
"DOWNLOAD": {
|
113
138
|
"auto_first": false,
|
114
139
|
"quality": "320K"
|
115
|
-
},
|
116
|
-
"BROWSER": {
|
117
|
-
"headless": true,
|
118
|
-
"timeout": 8
|
119
140
|
}
|
120
141
|
}
|
121
142
|
```
|
@@ -130,10 +151,6 @@ SpotDown uses a JSON configuration file with the following structure:
|
|
130
151
|
- **`auto_first`**: Automatically select first search result
|
131
152
|
- **`quality`**: Audio quality (320K recommended for best quality)
|
132
153
|
|
133
|
-
#### BROWSER Settings
|
134
|
-
- **`headless`**: Run browser in background (recommended: true)
|
135
|
-
- **`timeout`**: Browser timeout in seconds
|
136
|
-
|
137
154
|
## Usage
|
138
155
|
|
139
156
|
### Starting SpotDown
|
@@ -10,6 +10,13 @@
|
|
10
10
|
|
11
11
|
[](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
|
12
12
|
|
13
|
+
## 🚀 Download & Install
|
14
|
+
|
15
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_win.exe)
|
16
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_mac)
|
17
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_latest)
|
18
|
+
[](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_previous)
|
19
|
+
|
13
20
|
---
|
14
21
|
|
15
22
|
*⚡ **Quick Start:** `pip install spotdown && spotdown`*
|
@@ -20,9 +27,9 @@
|
|
20
27
|
|
21
28
|
- [✨ Features](#features)
|
22
29
|
- [🛠️ Installation](#️installation)
|
30
|
+
- [⚙️ Setup](#setup)
|
23
31
|
- [⚙️ Configuration](#configuration)
|
24
32
|
- [💻 Usage](#usage)
|
25
|
-
- [⚠️ Disclaimer](#disclaimer)
|
26
33
|
|
27
34
|
## Features
|
28
35
|
|
@@ -67,6 +74,24 @@ After installation, run this one-time setup command:
|
|
67
74
|
playwright install chromium
|
68
75
|
```
|
69
76
|
|
77
|
+
## Setup
|
78
|
+
|
79
|
+
1. Go to the [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
|
80
|
+
2. Log in and create a new application
|
81
|
+
3. Copy your **Client ID** and **Client Secret**
|
82
|
+
4. Create a file named `.env` in the SpotDown directory with the following content:
|
83
|
+
|
84
|
+
```
|
85
|
+
SPOTIFY_CLIENT_ID=your_client_id_here
|
86
|
+
SPOTIPY_CLIENT_SECRET=your_client_secret_here
|
87
|
+
```
|
88
|
+
|
89
|
+
5. Save the file. SpotDown will automatically load these credentials.
|
90
|
+
|
91
|
+
### Error Handling
|
92
|
+
- If the credentials are missing, SpotDown will log an error and exit.
|
93
|
+
- If the credentials are invalid, SpotDown will log an error and exit. Please double-check your `.env` file and credentials.
|
94
|
+
|
70
95
|
## Configuration
|
71
96
|
|
72
97
|
SpotDown uses a JSON configuration file with the following structure:
|
@@ -80,10 +105,6 @@ SpotDown uses a JSON configuration file with the following structure:
|
|
80
105
|
"DOWNLOAD": {
|
81
106
|
"auto_first": false,
|
82
107
|
"quality": "320K"
|
83
|
-
},
|
84
|
-
"BROWSER": {
|
85
|
-
"headless": true,
|
86
|
-
"timeout": 8
|
87
108
|
}
|
88
109
|
}
|
89
110
|
```
|
@@ -98,10 +119,6 @@ SpotDown uses a JSON configuration file with the following structure:
|
|
98
119
|
- **`auto_first`**: Automatically select first search result
|
99
120
|
- **`quality`**: Audio quality (320K recommended for best quality)
|
100
121
|
|
101
|
-
#### BROWSER Settings
|
102
|
-
- **`headless`**: Run browser in background (recommended: true)
|
103
|
-
- **`timeout`**: Browser timeout in seconds
|
104
|
-
|
105
122
|
## Usage
|
106
123
|
|
107
124
|
### Starting SpotDown
|
@@ -15,7 +15,7 @@ from rich.console import Console
|
|
15
15
|
|
16
16
|
# Internal utils
|
17
17
|
from SpotDown.utils.config_json import config_manager
|
18
|
-
from SpotDown.utils.file_utils import
|
18
|
+
from SpotDown.utils.file_utils import file_utils
|
19
19
|
|
20
20
|
|
21
21
|
# Variable
|
@@ -25,7 +25,7 @@ quality = config_manager.get("DOWNLOAD", "quality")
|
|
25
25
|
class YouTubeDownloader:
|
26
26
|
def __init__(self):
|
27
27
|
self.console = Console()
|
28
|
-
self.file_utils =
|
28
|
+
self.file_utils = file_utils
|
29
29
|
|
30
30
|
def download(self, video_info: Dict, spotify_info: Dict) -> bool:
|
31
31
|
"""
|
@@ -87,6 +87,7 @@ class YouTubeDownloader:
|
|
87
87
|
'--no-playlist',
|
88
88
|
'--embed-metadata',
|
89
89
|
'--add-metadata',
|
90
|
+
'--ffmpeg-location', self.file_utils.ffmpeg_path
|
90
91
|
]
|
91
92
|
|
92
93
|
if cover_path and cover_path.exists():
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# 05.04.2024
|
2
|
+
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import sys
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
from typing import Dict, List, Optional
|
9
|
+
from dotenv import load_dotenv
|
10
|
+
|
11
|
+
|
12
|
+
# External library
|
13
|
+
import spotipy
|
14
|
+
from spotipy.oauth2 import SpotifyClientCredentials
|
15
|
+
from rich.console import Console
|
16
|
+
from rich.progress import Progress
|
17
|
+
|
18
|
+
|
19
|
+
# Variable
|
20
|
+
console = Console()
|
21
|
+
load_dotenv()
|
22
|
+
|
23
|
+
|
24
|
+
def extract_track_id(spotify_url):
|
25
|
+
patterns = [
|
26
|
+
r'track/([a-zA-Z0-9]{22})',
|
27
|
+
r'spotify:track:([a-zA-Z0-9]{22})'
|
28
|
+
]
|
29
|
+
for pattern in patterns:
|
30
|
+
match = re.search(pattern, spotify_url)
|
31
|
+
if match:
|
32
|
+
return match.group(1)
|
33
|
+
return None
|
34
|
+
|
35
|
+
|
36
|
+
def extract_playlist_id(spotify_url):
|
37
|
+
patterns = [
|
38
|
+
r'playlist/([a-zA-Z0-9]{22})',
|
39
|
+
r'spotify:playlist:([a-zA-Z0-9]{22})'
|
40
|
+
]
|
41
|
+
for pattern in patterns:
|
42
|
+
match = re.search(pattern, spotify_url)
|
43
|
+
if match:
|
44
|
+
return match.group(1)
|
45
|
+
return None
|
46
|
+
|
47
|
+
|
48
|
+
class SpotifyExtractor:
|
49
|
+
def __init__(self):
|
50
|
+
client_id = os.getenv("SPOTIPY_CLIENT_ID")
|
51
|
+
client_secret = os.getenv("SPOTIPY_CLIENT_SECRET")
|
52
|
+
|
53
|
+
if not client_id or not client_secret:
|
54
|
+
console.print("[red]Missing Spotify credentials. Please create a .env file with SPOTIFY_CLIENT_ID and SPOTIPY_CLIENT_SECRET from https://developer.spotify.com/dashboard/")
|
55
|
+
sys.exit(1)
|
56
|
+
|
57
|
+
self.sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials(
|
58
|
+
client_id=client_id,
|
59
|
+
client_secret=client_secret
|
60
|
+
))
|
61
|
+
logging.info("SpotifyExtractor initialized")
|
62
|
+
|
63
|
+
def __enter__(self):
|
64
|
+
return self
|
65
|
+
|
66
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
67
|
+
pass
|
68
|
+
|
69
|
+
def extract_track_info(self, spotify_url: str, save_json: bool = False) -> Optional[Dict]:
|
70
|
+
track_id = extract_track_id(spotify_url)
|
71
|
+
if not track_id:
|
72
|
+
logging.error("Invalid Spotify track URL")
|
73
|
+
return None
|
74
|
+
|
75
|
+
try:
|
76
|
+
# Extract track info
|
77
|
+
track = self.sp.track(track_id)
|
78
|
+
|
79
|
+
# Extract album info
|
80
|
+
album = track['album']
|
81
|
+
|
82
|
+
# Process extracted data
|
83
|
+
release_date = album['release_date']
|
84
|
+
year = release_date.split('-')[0] if release_date else None
|
85
|
+
|
86
|
+
# Extract duration in seconds and formatted
|
87
|
+
duration_ms = track['duration_ms']
|
88
|
+
duration_seconds = duration_ms // 1000 if duration_ms else None
|
89
|
+
duration_formatted = f"{duration_seconds // 60}:{duration_seconds % 60:02d}" if duration_seconds else None
|
90
|
+
|
91
|
+
# Extract cover URL
|
92
|
+
cover_url = album['images'][0]['url'] if album['images'] else None
|
93
|
+
|
94
|
+
# Extract artists
|
95
|
+
artists = [artist['name'] for artist in track['artists']]
|
96
|
+
|
97
|
+
# Compile track info
|
98
|
+
track_info = {
|
99
|
+
'artist': ', '.join(artists),
|
100
|
+
'title': track['name'],
|
101
|
+
'album': album['name'],
|
102
|
+
'year': year,
|
103
|
+
'duration_seconds': duration_seconds,
|
104
|
+
'duration_formatted': duration_formatted,
|
105
|
+
'cover_url': cover_url
|
106
|
+
}
|
107
|
+
|
108
|
+
if save_json:
|
109
|
+
log_dir = os.path.join(os.getcwd(), "log")
|
110
|
+
os.makedirs(log_dir, exist_ok=True)
|
111
|
+
|
112
|
+
# Create JSON file for track info
|
113
|
+
filename = f"{track_info['artist']} - {track_info['title']}.json"
|
114
|
+
filepath = os.path.join(log_dir, filename)
|
115
|
+
|
116
|
+
# Save track info to JSON
|
117
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
118
|
+
json.dump(track_info, f, ensure_ascii=False, indent=2)
|
119
|
+
|
120
|
+
return track_info
|
121
|
+
except Exception as e:
|
122
|
+
error_msg = str(e)
|
123
|
+
logging.error(f"Spotify extraction error: {error_msg}")
|
124
|
+
|
125
|
+
if "invalid_client" in error_msg:
|
126
|
+
console.print("[red]Spotify credentials are invalid. Please check your .env file and obtain valid credentials from https://developer.spotify.com/dashboard/. Exiting.")
|
127
|
+
sys.exit(0)
|
128
|
+
|
129
|
+
return None
|
130
|
+
|
131
|
+
def extract_playlist_tracks(self, playlist_url: str) -> List[Dict]:
|
132
|
+
playlist_id = extract_playlist_id(playlist_url)
|
133
|
+
|
134
|
+
if not playlist_id:
|
135
|
+
logging.error("Invalid Spotify playlist URL")
|
136
|
+
return []
|
137
|
+
|
138
|
+
try:
|
139
|
+
|
140
|
+
# Extract playlist info
|
141
|
+
playlist = self.sp.playlist(playlist_id)
|
142
|
+
total_tracks = playlist['tracks']['total']
|
143
|
+
tracks_info = []
|
144
|
+
offset = 0
|
145
|
+
limit = 100
|
146
|
+
console.print(f"[green]Playlist has [red]{total_tracks}[/red] tracks.")
|
147
|
+
|
148
|
+
with Progress() as progress:
|
149
|
+
task = progress.add_task("[cyan]Extracting tracks...", total=total_tracks)
|
150
|
+
|
151
|
+
while offset < total_tracks:
|
152
|
+
progress.update(task, advance=0, description=f"[cyan]Loading tracks {offset + 1}-{min(offset + limit, total_tracks)} of {total_tracks}...")
|
153
|
+
results = self.sp.playlist_items(
|
154
|
+
playlist_id,
|
155
|
+
offset=offset,
|
156
|
+
limit=limit,
|
157
|
+
fields='items(track(name,artists(name),album(name,release_date,images),duration_ms))'
|
158
|
+
)
|
159
|
+
|
160
|
+
if not results['items']:
|
161
|
+
break
|
162
|
+
|
163
|
+
for idx, item in enumerate(results['items']):
|
164
|
+
if item['track'] is None:
|
165
|
+
continue
|
166
|
+
|
167
|
+
# Extract track details
|
168
|
+
track = item['track']
|
169
|
+
|
170
|
+
# Extract album info
|
171
|
+
album = track['album']
|
172
|
+
|
173
|
+
# Process extracted data
|
174
|
+
#release_date = album['release_date']
|
175
|
+
#year = release_date.split('-')[0] if release_date else None
|
176
|
+
|
177
|
+
# Extract duration in seconds
|
178
|
+
duration_ms = track['duration_ms']
|
179
|
+
duration_seconds = duration_ms // 1000 if duration_ms else None
|
180
|
+
|
181
|
+
# Extract cover URL
|
182
|
+
cover_url = album['images'][0]['url'] if album['images'] else None
|
183
|
+
|
184
|
+
# Extract artists
|
185
|
+
artists = [artist['name'] for artist in track['artists']]
|
186
|
+
|
187
|
+
# Compile track info
|
188
|
+
track_info = {
|
189
|
+
"title": track['name'],
|
190
|
+
"artist": ', '.join(artists),
|
191
|
+
"album": album['name'],
|
192
|
+
"added_at": None,
|
193
|
+
"cover_art": cover_url,
|
194
|
+
"duration_ms": duration_ms,
|
195
|
+
"duration_seconds": duration_seconds,
|
196
|
+
"play_count": None
|
197
|
+
}
|
198
|
+
|
199
|
+
# Append to list
|
200
|
+
tracks_info.append(track_info)
|
201
|
+
progress.update(task, advance=1)
|
202
|
+
offset += limit
|
203
|
+
|
204
|
+
# Remove duplicates based on title and artist
|
205
|
+
unique = {}
|
206
|
+
for item in tracks_info:
|
207
|
+
key = (item.get("title", ""), item.get("artist", ""))
|
208
|
+
if key not in unique:
|
209
|
+
unique[key] = item
|
210
|
+
|
211
|
+
# Convert back to list
|
212
|
+
unique_tracks = list(unique.values())
|
213
|
+
console.print(f"[green]Extracted [red]{len(unique_tracks)}[/red] unique tracks from playlist")
|
214
|
+
return unique_tracks
|
215
|
+
|
216
|
+
except Exception as e:
|
217
|
+
logging.error(f"Error extracting playlist: {e}")
|
218
|
+
return []
|
@@ -6,6 +6,7 @@ from typing import Dict, List, Optional
|
|
6
6
|
|
7
7
|
# Internal utils
|
8
8
|
from SpotDown.utils.logger import Logger
|
9
|
+
from SpotDown.utils.file_utils import file_utils
|
9
10
|
from SpotDown.utils.console_utils import ConsoleUtils
|
10
11
|
from SpotDown.upload.update import update as git_update
|
11
12
|
from SpotDown.extractor.spotify_extractor import SpotifyExtractor
|
@@ -109,6 +110,7 @@ def run():
|
|
109
110
|
console = ConsoleUtils()
|
110
111
|
console.start_message()
|
111
112
|
git_update()
|
113
|
+
file_utils.get_system_summary()
|
112
114
|
|
113
115
|
spotify_url = console.get_spotify_url()
|
114
116
|
max_results = 5
|
@@ -157,7 +157,7 @@ class ConsoleUtils:
|
|
157
157
|
str: Entered Spotify URL
|
158
158
|
"""
|
159
159
|
while True:
|
160
|
-
url = Prompt.ask("[purple]Enter Spotify URL[/purple][green]").strip()
|
160
|
+
url = Prompt.ask("\n[purple]Enter Spotify URL[/purple][green]").strip()
|
161
161
|
|
162
162
|
if not url:
|
163
163
|
self.console.print("[red]URL cannot be empty. Please enter a Spotify track URL.[/red]")
|