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.
Files changed (30) hide show
  1. {spotdown-1.0.0/SpotDown.egg-info → spotdown-1.3.0}/PKG-INFO +28 -11
  2. {spotdown-1.0.0 → spotdown-1.3.0}/README.md +26 -9
  3. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/downloader/youtube_downloader.py +3 -2
  4. spotdown-1.3.0/SpotDown/extractor/spotify_extractor.py +218 -0
  5. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/main.py +2 -0
  6. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/upload/version.py +1 -1
  7. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/console_utils.py +1 -1
  8. spotdown-1.3.0/SpotDown/utils/ffmpeg_installer.py +374 -0
  9. spotdown-1.3.0/SpotDown/utils/file_utils.py +233 -0
  10. {spotdown-1.0.0 → spotdown-1.3.0/SpotDown.egg-info}/PKG-INFO +28 -11
  11. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/SOURCES.txt +1 -0
  12. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/requires.txt +1 -1
  13. {spotdown-1.0.0 → spotdown-1.3.0}/requirements.txt +2 -2
  14. spotdown-1.0.0/SpotDown/extractor/spotify_extractor.py +0 -355
  15. spotdown-1.0.0/SpotDown/utils/file_utils.py +0 -129
  16. {spotdown-1.0.0 → spotdown-1.3.0}/LICENSE +0 -0
  17. {spotdown-1.0.0 → spotdown-1.3.0}/MANIFEST.in +0 -0
  18. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/__init__.py +0 -0
  19. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/downloader/__init__.py +0 -0
  20. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/extractor/__init__.py +0 -0
  21. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/extractor/youtube_extractor.py +0 -0
  22. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/__init__.py +0 -0
  23. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/config_json.py +0 -0
  24. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/headers.py +0 -0
  25. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown/utils/logger.py +0 -0
  26. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/dependency_links.txt +0 -0
  27. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/entry_points.txt +0 -0
  28. {spotdown-1.0.0 → spotdown-1.3.0}/SpotDown.egg-info/top_level.txt +0 -0
  29. {spotdown-1.0.0 → spotdown-1.3.0}/setup.cfg +0 -0
  30. {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.0.0
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
  [![Donate PayPal](https://img.shields.io/badge/💳_Donate-PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white&labelColor=2d3748)](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
44
44
 
45
+ ## 🚀 Download & Install
46
+
47
+ [![Windows](https://img.shields.io/badge/🪟_Windows-0078D4?style=for-the-badge&logo=windows&logoColor=white&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_win.exe)
48
+ [![macOS](https://img.shields.io/badge/🍎_macOS-000000?style=for-the-badge&logo=apple&logoColor=white&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_mac)
49
+ [![Linux latest](https://img.shields.io/badge/🐧_Linux_latest-FCC624?style=for-the-badge&logo=linux&logoColor=black&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_latest)
50
+ [![Linux 22.04](https://img.shields.io/badge/🐧_Linux_22.04-FCC624?style=for-the-badge&logo=linux&logoColor=black&labelColor=2d3748)](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
  [![Donate PayPal](https://img.shields.io/badge/💳_Donate-PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white&labelColor=2d3748)](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
12
12
 
13
+ ## 🚀 Download & Install
14
+
15
+ [![Windows](https://img.shields.io/badge/🪟_Windows-0078D4?style=for-the-badge&logo=windows&logoColor=white&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_win.exe)
16
+ [![macOS](https://img.shields.io/badge/🍎_macOS-000000?style=for-the-badge&logo=apple&logoColor=white&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_mac)
17
+ [![Linux latest](https://img.shields.io/badge/🐧_Linux_latest-FCC624?style=for-the-badge&logo=linux&logoColor=black&labelColor=2d3748)](https://github.com/Arrowar/spotdown/releases/latest/download/spotdown_linux_latest)
18
+ [![Linux 22.04](https://img.shields.io/badge/🐧_Linux_22.04-FCC624?style=for-the-badge&logo=linux&logoColor=black&labelColor=2d3748)](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 FileUtils
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 = FileUtils()
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
@@ -1,5 +1,5 @@
1
1
  __title__ = 'SpotDown'
2
- __version__ = '1.0.0'
2
+ __version__ = '1.3.0'
3
3
  __author__ = 'Arrowar'
4
4
  __description__ = 'A command-line program to download music'
5
5
  __copyright__ = 'Copyright 2025'
@@ -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]")