SpotDown 0.1.0__py3-none-any.whl → 1.0.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.
@@ -1,6 +1,7 @@
1
1
  # 05.04.2024
2
2
 
3
3
  import io
4
+ import logging
4
5
  import subprocess
5
6
  from typing import Dict
6
7
 
@@ -44,6 +45,7 @@ class YouTubeDownloader:
44
45
  spotify_info.get('title', video_info.get('title', 'Unknown Title'))
45
46
  )
46
47
  output_path = music_folder / f"{filename}.%(ext)s"
48
+ logging.info(f"Start download: {video_info.get('url')} as {output_path}")
47
49
 
48
50
  # Download cover image if available
49
51
  cover_path = None
@@ -65,12 +67,15 @@ class YouTubeDownloader:
65
67
  img.save(cover_path, "JPEG")
66
68
 
67
69
  self.console.print(f"[blue]Downloaded thumbnail: {cover_path}[/blue]")
70
+ logging.info(f"Downloaded thumbnail: {cover_path}")
68
71
 
69
72
  else:
70
73
  cover_path = None
74
+ logging.warning(f"Failed to download cover image, status code: {resp.status_code}")
71
75
 
72
76
  except Exception as e:
73
77
  self.console.print(f"[yellow]Unable to download cover: {e}[/yellow]")
78
+ logging.error(f"Unable to download cover: {e}")
74
79
  cover_path = None
75
80
 
76
81
  ytdlp_options = [
@@ -94,6 +99,7 @@ class YouTubeDownloader:
94
99
  console=self.console
95
100
  ) as progress:
96
101
  task = progress.add_task("Downloading...", total=None)
102
+ logging.info(f"Running yt-dlp with options: {ytdlp_options}")
97
103
  process = subprocess.run(
98
104
  ytdlp_options,
99
105
  capture_output=True,
@@ -102,30 +108,37 @@ class YouTubeDownloader:
102
108
  progress.remove_task(task)
103
109
 
104
110
  if process.returncode == 0:
111
+ logging.info("yt-dlp finished successfully")
105
112
 
106
113
  # Find the downloaded file
107
114
  downloaded_files = list(music_folder.glob(f"{filename}.*"))
108
115
  if downloaded_files:
109
116
  self.console.print("[red]Download completed![/red]")
117
+ logging.info(f"Download completed: {downloaded_files[0]}")
110
118
 
111
119
  # Remove cover file after embedding
112
120
  if cover_path and cover_path.exists():
113
121
  try:
114
122
  cover_path.unlink()
115
- except Exception:
116
- pass
117
-
123
+ logging.info(f"Removed temporary cover file: {cover_path}")
124
+
125
+ except Exception as ex:
126
+ logging.warning(f"Failed to remove cover file: {ex}")
127
+
118
128
  return True
119
129
 
120
130
  else:
121
131
  self.console.print("[yellow]Download apparently succeeded but file not found[/yellow]")
132
+ logging.error("Download apparently succeeded but file not found")
122
133
  return False
123
-
134
+
124
135
  else:
125
136
  self.console.print("[red]Download error:[/red]")
126
137
  self.console.print(f"[red]{process.stderr}[/red]")
138
+ logging.error(f"yt-dlp error: {process.stderr}")
127
139
  return False
128
140
 
129
141
  except Exception as e:
130
142
  self.console.print(f"[red]Error during download: {e}[/red]")
143
+ logging.error(f"Error during download: {e}")
131
144
  return False
@@ -31,9 +31,11 @@ class SpotifyExtractor:
31
31
  self.user_agent = get_userAgent()
32
32
  self.total_songs = None
33
33
  self.playlist_items = []
34
+ logging.info("SpotifyExtractor initialized")
34
35
 
35
36
  def __enter__(self):
36
37
  """Context manager to automatically handle the browser"""
38
+ logging.info("Starting Playwright and launching browser")
37
39
  self.playwright = sync_playwright().start()
38
40
  self.browser = self.playwright.chromium.launch(headless=headless)
39
41
  self.context = self.browser.new_context(
@@ -44,6 +46,7 @@ class SpotifyExtractor:
44
46
 
45
47
  def __exit__(self, exc_type, exc_val, exc_tb):
46
48
  """Automatically closes the browser"""
49
+ logging.info("Closing browser and stopping Playwright")
47
50
  if self.browser:
48
51
  self.browser.close()
49
52
  if self.playwright:
@@ -61,18 +64,21 @@ class SpotifyExtractor:
61
64
  Dict: Track information or None if an error occurs
62
65
  """
63
66
  try:
67
+ logging.info(f"Analyzing Spotify URL: {spotify_url}")
64
68
  console.print("[cyan]Analyzing Spotify URL ...")
65
69
 
66
70
  # Extract Spotify data by intercepting API calls
67
71
  spotify_data, raw_json = self._extract_spotify_data(spotify_url, return_raw=True)
68
72
 
69
73
  if not spotify_data:
74
+ logging.info("Unable to extract data from Spotify")
70
75
  console.print("[cyan]Unable to extract data from Spotify")
71
76
  return None
72
77
 
73
78
  # Save the JSON response if requested
74
79
  if save_json and raw_json:
75
80
  try:
81
+ logging.info("Saving Spotify API response JSON")
76
82
  log_dir = os.path.join(os.getcwd(), "log")
77
83
  os.makedirs(log_dir, exist_ok=True)
78
84
 
@@ -91,18 +97,22 @@ class SpotifyExtractor:
91
97
  console.print(f"[green]Spotify API response saved to {filepath}")
92
98
 
93
99
  except Exception as e:
100
+ logging.error(f"Could not save JSON file: {e}")
94
101
  console.print(f"[yellow]Warning: Could not save JSON file: {e}")
95
102
 
103
+ logging.info(f"Found track: {spotify_data['artist']} - {spotify_data['title']}")
96
104
  console.print(f"[cyan]Found: [red]{spotify_data['artist']} - {spotify_data['title']}[/red]")
97
105
  return spotify_data
98
106
 
99
107
  except Exception as e:
108
+ logging.error(f"Spotify extraction error: {e}")
100
109
  console.print(f"[cyan]Spotify extraction error: {e}")
101
110
  return None
102
111
 
103
112
  def _extract_spotify_data(self, spotify_url: str, return_raw: bool = False) -> Optional[Dict]:
104
113
  """Extracts Spotify data by intercepting API calls"""
105
114
  try:
115
+ logging.info(f"Intercepting API calls for URL: {spotify_url}")
106
116
  api_responses = []
107
117
 
108
118
  def handle_request(request):
@@ -130,20 +140,24 @@ class SpotifyExtractor:
130
140
  # This avoids unnecessary waiting after a valid API response is received
131
141
  for _ in range(timeout * 10): # 100 * 100ms = 10000ms (10 seconds max)
132
142
  if api_responses:
143
+ logging.info("Valid API response found, stopping polling")
133
144
  break
134
145
 
135
146
  self.page.wait_for_timeout(timeout * 10)
136
147
 
137
148
  if not api_responses:
149
+ logging.info("No valid API responses found")
138
150
  console.print("[cyan]No valid API responses found")
139
151
  return (None, None) if return_raw else None
140
152
 
141
153
  # Selects the most complete response
142
154
  best_response = max(api_responses, key=lambda x: len(json.dumps(x)))
143
155
  parsed = self._parse_spotify_response(best_response)
156
+ logging.info("Returning parsed Spotify API response")
144
157
  return (parsed, best_response) if return_raw else parsed
145
158
 
146
159
  except Exception as e:
160
+ logging.error(f"Spotify data extraction error: {e}")
147
161
  console.print(f"[cyan]❌ Spotify data extraction error: {e}")
148
162
  return (None, None) if return_raw else None
149
163
 
@@ -154,6 +168,7 @@ class SpotifyExtractor:
154
168
  return bool(track_union.get("name") and track_union.get("firstArtist", {}).get("items"))
155
169
 
156
170
  except Exception:
171
+ logging.error("Error validating track data")
157
172
  return False
158
173
 
159
174
  def _parse_spotify_response(self, response: Dict) -> Dict:
@@ -202,6 +217,7 @@ class SpotifyExtractor:
202
217
  }
203
218
 
204
219
  except Exception as e:
220
+ logging.error(f"Error parsing Spotify response: {e}")
205
221
  console.print(f"[cyan]Error parsing Spotify response: {e}")
206
222
  return {}
207
223
 
@@ -220,6 +236,7 @@ class SpotifyExtractor:
220
236
 
221
237
  def extract_playlist_tracks(self, playlist_url: str) -> List[Dict]:
222
238
  """Extracts all tracks from a Spotify playlist URL"""
239
+ logging.info(f"Extracting playlist tracks from: {playlist_url}")
223
240
  self.total_songs = None
224
241
  self.playlist_items = []
225
242
  console.print("[cyan]Extracting playlist tracks...")
@@ -242,6 +259,7 @@ class SpotifyExtractor:
242
259
  if parsed_item:
243
260
  self.playlist_items.append(parsed_item)
244
261
  except Exception as e:
262
+ logging.error(f"Error processing playlist request: {e}")
245
263
  console.print(f"Error processing request: {e}")
246
264
 
247
265
  self.page.on("response", handle_request)
@@ -249,14 +267,17 @@ class SpotifyExtractor:
249
267
  self.page.wait_for_timeout(5000)
250
268
 
251
269
  if self.total_songs is None:
270
+ logging.error("Could not extract the total number of songs")
252
271
  console.print("Error: Could not extract the total number of songs.")
253
272
  return []
254
273
 
274
+ logging.info(f"Playlist has {self.total_songs} tracks")
255
275
  console.print(f"[cyan]The playlist has [green]{self.total_songs}[/green] tracks")
256
276
 
257
277
  try:
258
278
  self.page.wait_for_selector('div[data-testid="playlist-tracklist"]', timeout=15000)
259
279
  except Exception:
280
+ logging.error("Playlist table did not load")
260
281
  console.print("Error: Playlist table did not load")
261
282
  return []
262
283
 
@@ -281,9 +302,11 @@ class SpotifyExtractor:
281
302
  unique[key] = item
282
303
 
283
304
  unique_tracks = list(unique.values())
305
+ logging.info(f"Extracted {len(unique_tracks)} unique tracks from playlist")
284
306
  return unique_tracks
285
307
 
286
308
  except Exception as e:
309
+ logging.error(f"Error extracting playlist: {e}")
287
310
  console.print(f"Error extracting playlist: {e}")
288
311
  return []
289
312
 
@@ -327,5 +350,6 @@ class SpotifyExtractor:
327
350
  }
328
351
 
329
352
  except Exception as e:
353
+ logging.error(f"Error parsing playlist item: {e}")
330
354
  console.print(f"Error parsing playlist item: {e}")
331
355
  return {}
@@ -3,6 +3,7 @@
3
3
  import re
4
4
  import json
5
5
  import difflib
6
+ import logging
6
7
  from urllib.parse import quote_plus
7
8
  from typing import Dict, List, Optional
8
9
 
@@ -42,6 +43,7 @@ class YouTubeExtractor:
42
43
  List[Dict]: List of found videos
43
44
  """
44
45
  try:
46
+ logging.info(f"Starting YouTube search for query: {query}")
45
47
  search_url = f"https://www.youtube.com/results?search_query={quote_plus(query)}"
46
48
  console.print(f"\n[bold blue]Searching on YouTube:[/bold blue] {query}")
47
49
 
@@ -49,9 +51,12 @@ class YouTubeExtractor:
49
51
  response = client.get(search_url, headers={"User-Agent": get_userAgent()})
50
52
  html = response.text
51
53
 
52
- return self._extract_youtube_videos(html, max_results)
54
+ results = self._extract_youtube_videos(html, max_results)
55
+ logging.info(f"Found {len(results)} results for query: {query}")
56
+ return results
53
57
 
54
58
  except Exception as e:
59
+ logging.error(f"YouTube search error: {e}")
55
60
  print(f"YouTube search error: {e}")
56
61
  return []
57
62
 
@@ -63,6 +68,7 @@ class YouTubeExtractor:
63
68
  youtube_results (List[Dict]): List of YouTube videos
64
69
  target_duration (int): Target duration in seconds
65
70
  """
71
+ logging.info(f"Sorting {len(youtube_results)} results by duration similarity to {target_duration}s")
66
72
  for result in youtube_results:
67
73
  if result.get('duration_seconds') is not None:
68
74
  result['duration_difference'] = abs(result['duration_seconds'] - target_duration)
@@ -80,6 +86,7 @@ class YouTubeExtractor:
80
86
  youtube_results (List[Dict]): List of YouTube videos
81
87
  spotify_info (Dict): Spotify track info
82
88
  """
89
+ logging.info(f"Sorting {len(youtube_results)} results by affinity and duration using Spotify info")
83
90
  target_duration = spotify_info.get('duration_seconds')
84
91
  target_title = spotify_info.get('title', '').lower()
85
92
  target_artist = spotify_info.get('artist', '').lower()
@@ -123,7 +130,9 @@ class YouTubeExtractor:
123
130
  """Extract videos from YouTube HTML"""
124
131
  try:
125
132
  yt_match = re.search(r'var ytInitialData = ({.+?});', html, re.DOTALL)
133
+
126
134
  if not yt_match:
135
+ logging.warning("ytInitialData not found in HTML")
127
136
  return []
128
137
 
129
138
  yt_data = json.loads(yt_match.group(1))
@@ -152,9 +161,11 @@ class YouTubeExtractor:
152
161
  if len(results) >= max_results:
153
162
  break
154
163
 
164
+ logging.info(f"Extracted {len(results)} video(s) from HTML")
155
165
  return results
156
166
 
157
167
  except Exception as e:
168
+ logging.error(f"Video extraction error: {e}")
158
169
  print(f"Video extraction error: {e}")
159
170
  return []
160
171
 
@@ -163,6 +174,7 @@ class YouTubeExtractor:
163
174
  try:
164
175
  video_id = video_data.get('videoId')
165
176
  if not video_id:
177
+ logging.warning("videoId not found in video_data")
166
178
  return None
167
179
 
168
180
  # Title
@@ -187,6 +199,7 @@ class YouTubeExtractor:
187
199
  # Published date
188
200
  published = self._extract_text(video_data.get('publishedTimeText', {}))
189
201
 
202
+ logging.info(f"Parsed video: {title} (ID: {video_id})")
190
203
  return {
191
204
  'video_id': video_id,
192
205
  'url': f'https://www.youtube.com/watch?v={video_id}',
@@ -200,6 +213,7 @@ class YouTubeExtractor:
200
213
  }
201
214
 
202
215
  except Exception as e:
216
+ logging.error(f"Video parsing error: {e}")
203
217
  print(f"Video parsing error: {e}")
204
218
  return None
205
219
 
SpotDown/main.py CHANGED
@@ -1,12 +1,13 @@
1
1
  # 05.04.2024
2
2
 
3
3
  import time
4
- import logging
5
4
  from typing import Dict, List, Optional
6
5
 
7
6
 
8
7
  # Internal utils
8
+ from SpotDown.utils.logger import Logger
9
9
  from SpotDown.utils.console_utils import ConsoleUtils
10
+ from SpotDown.upload.update import update as git_update
10
11
  from SpotDown.extractor.spotify_extractor import SpotifyExtractor
11
12
  from SpotDown.extractor.youtube_extractor import YouTubeExtractor
12
13
  from SpotDown.downloader.youtube_downloader import YouTubeDownloader
@@ -17,11 +18,6 @@ from SpotDown.downloader.youtube_downloader import YouTubeDownloader
17
18
  console = ConsoleUtils()
18
19
 
19
20
 
20
- def setup_logging():
21
- """Initialize basic logging configuration"""
22
- logging.basicConfig(level=logging.ERROR)
23
-
24
-
25
21
  def extract_spotify_data(spotify_url: str, max_retry: int = 3) -> Optional[Dict]:
26
22
  """Extract data from Spotify URL with retry mechanism"""
27
23
  for attempt in range(1, max_retry + 1):
@@ -108,11 +104,11 @@ def handle_single_track_download(spotify_info: Dict, max_results: int):
108
104
 
109
105
  def run():
110
106
  """Main execution function"""
111
- setup_logging()
107
+ Logger()
112
108
 
113
109
  console = ConsoleUtils()
114
110
  console.start_message()
115
- #git_update()
111
+ git_update()
116
112
 
117
113
  spotify_url = console.get_spotify_url()
118
114
  max_results = 5
@@ -0,0 +1,5 @@
1
+ __title__ = 'SpotDown'
2
+ __version__ = '1.0.0'
3
+ __author__ = 'Arrowar'
4
+ __description__ = 'A command-line program to download music'
5
+ __copyright__ = 'Copyright 2025'
@@ -44,7 +44,7 @@ class ConfigManager:
44
44
 
45
45
  def download_config(self) -> None:
46
46
  """Download config.json from the Arrowar/SpotDown GitHub repository."""
47
- url = "https://raw.githubusercontent.com/Arrowar/SpotDown/main/config.json"
47
+ url = "https://raw.githubusercontent.com/Arrowar/SpotDown/refs/heads/main/config.json"
48
48
  try:
49
49
  with httpx.Client(timeout=10, headers=get_headers()) as client:
50
50
  response = client.get(url)
@@ -220,4 +220,4 @@ class ConfigManager:
220
220
  return section in config_source
221
221
 
222
222
 
223
- config_manager = ConfigManager()
223
+ config_manager = ConfigManager()
@@ -0,0 +1,90 @@
1
+ # 05.04.2024
2
+
3
+ import os
4
+ import logging
5
+ from logging.handlers import RotatingFileHandler
6
+
7
+
8
+ # Internal utils
9
+ from SpotDown.utils.config_json import config_manager
10
+
11
+
12
+
13
+ class Logger:
14
+ _instance = None
15
+
16
+ def __new__(cls):
17
+ # Singleton pattern to avoid multiple logger instances
18
+ if cls._instance is None:
19
+ cls._instance = super(Logger, cls).__new__(cls)
20
+ cls._instance._initialized = False
21
+ return cls._instance
22
+
23
+ def __init__(self):
24
+ # Initialize only once
25
+ if getattr(self, '_initialized', False):
26
+ return
27
+
28
+ # Configure root logger
29
+ self.debug_mode = config_manager.get_bool('DEFAULT', "debug")
30
+ self.logger = logging.getLogger('')
31
+
32
+ # Remove any existing handlers to avoid duplication
33
+ for handler in self.logger.handlers[:]:
34
+ self.logger.removeHandler(handler)
35
+
36
+ # Reduce logging level for external libraries
37
+ logging.getLogger("httpx").setLevel(logging.WARNING)
38
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
39
+
40
+ # Set logging level based on debug_mode
41
+ if self.debug_mode:
42
+ self.logger.setLevel(logging.DEBUG)
43
+ # In debug mode: log SOLO nel file, non nel terminale
44
+ self._configure_console_log_file()
45
+ else:
46
+ self.logger.setLevel(logging.ERROR)
47
+ # In modalità normale: log solo nel terminale per gli errori
48
+ self._configure_console_logging()
49
+
50
+ self._initialized = True
51
+
52
+ def _configure_console_logging(self):
53
+ """Configure console logging output to terminal."""
54
+ console_handler = logging.StreamHandler()
55
+ console_handler.setLevel(logging.ERROR) # Solo errori nel terminale
56
+ formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
57
+ console_handler.setFormatter(formatter)
58
+ self.logger.addHandler(console_handler)
59
+
60
+ def _configure_console_log_file(self):
61
+ """Create a console.log file only when debug mode is enabled."""
62
+ console_log_path = "console.log"
63
+ try:
64
+ # Remove existing file if present
65
+ if os.path.exists(console_log_path):
66
+ os.remove(console_log_path)
67
+
68
+ # Create handler for console.log
69
+ console_file_handler = RotatingFileHandler(
70
+ console_log_path,
71
+ maxBytes=5*1024*1024, # 5 MB
72
+ backupCount=3
73
+ )
74
+ console_file_handler.setLevel(logging.DEBUG)
75
+ formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s')
76
+ console_file_handler.setFormatter(formatter)
77
+ self.logger.addHandler(console_file_handler)
78
+
79
+ except Exception as e:
80
+ print(f"Error creating console.log: {e}")
81
+
82
+ @staticmethod
83
+ def get_logger(name=None):
84
+ """
85
+ Get a specific logger for a module/component.
86
+ If name is None, returns the root logger.
87
+ """
88
+ # Ensure Logger instance is initialized
89
+ Logger()
90
+ return logging.getLogger(name)
@@ -1,20 +1,34 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SpotDown
3
- Version: 0.1.0
4
- Home-page: https://github.com/Arrowar/SpotDown
3
+ Version: 1.0.0
4
+ Summary: A command-line program to download music
5
+ Home-page: https://github.com/Arrowar/spotdown
5
6
  Author: Arrowar
6
- Project-URL: Bug Reports, https://github.com/Arrowar/SpotDown/issues
7
- Project-URL: Source, https://github.com/Arrowar/SpotDown
7
+ Author-email: author@example.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: Operating System :: OS Independent
8
11
  Requires-Python: >=3.8
9
12
  Description-Content-Type: text/markdown
10
13
  License-File: LICENSE
14
+ Requires-Dist: rich
15
+ Requires-Dist: httpx
16
+ Requires-Dist: playwright
17
+ Requires-Dist: unidecode
18
+ Requires-Dist: ua-generator
19
+ Requires-Dist: unidecode
20
+ Requires-Dist: yt-dlp
21
+ Requires-Dist: Pillow
11
22
  Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
12
25
  Dynamic: description
13
26
  Dynamic: description-content-type
14
27
  Dynamic: home-page
15
28
  Dynamic: license-file
16
- Dynamic: project-url
29
+ Dynamic: requires-dist
17
30
  Dynamic: requires-python
31
+ Dynamic: summary
18
32
 
19
33
  <div align="center">
20
34
 
@@ -27,16 +41,10 @@ Dynamic: requires-python
27
41
  ## 💝 Support the Project
28
42
 
29
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)
30
- ## 🚀 Download & Install
31
-
32
- [![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)
33
- [![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)
34
- [![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)
35
- [![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)
36
44
 
37
45
  ---
38
46
 
39
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
47
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
40
48
 
41
49
  </div>
42
50
 
@@ -54,36 +62,41 @@ Dynamic: requires-python
54
62
  - 📋 **Download entire playlists** with ease
55
63
  - 🔍 **No authentication required** - uses web scraping
56
64
  - 🎨 **Automatic cover art embedding** (JPEG format)
65
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
57
66
 
58
67
  ## Installation
59
68
 
60
- ### Prerequisites
61
-
62
- - **Python 3.8+**
63
- - **FFmpeg** (for audio processing)
64
- - **yt-dlp** (for downloading)
65
-
66
- ### 1. Install Python Dependencies
69
+ ### Method 1: PyPI (Recommended)
67
70
 
68
71
  ```bash
69
- pip install -r requirements.txt
72
+ pip install spotdown
70
73
  ```
71
74
 
72
- ### 2. Install Playwright Chromium
75
+ That's it! You can now run `spotdown` from anywhere in your terminal.
76
+
77
+ ### Method 2: From Source
78
+
79
+ If you prefer to install from source:
73
80
 
74
81
  ```bash
75
- playwright install chromium
82
+ git clone https://github.com/Arrowar/spotdown.git
83
+ cd spotdown
84
+ pip install -e .
76
85
  ```
77
86
 
78
- ### 3. Quick Start
87
+ ### Prerequisites
79
88
 
80
- Create a simple launcher script:
89
+ The following dependencies will be automatically installed:
81
90
 
82
- ```python
83
- from spotdown.run import main
91
+ - **Python 3.8+**
92
+ - **FFmpeg** (for audio processing)
93
+ - **yt-dlp** (for downloading)
94
+ - **Playwright** (for web scraping)
95
+
96
+ After installation, run this one-time setup command:
84
97
 
85
- if __name__ == "__main__":
86
- main()
98
+ ```bash
99
+ playwright install chromium
87
100
  ```
88
101
 
89
102
  ## Configuration
@@ -102,7 +115,7 @@ SpotDown uses a JSON configuration file with the following structure:
102
115
  },
103
116
  "BROWSER": {
104
117
  "headless": true,
105
- "timeout": 6
118
+ "timeout": 8
106
119
  }
107
120
  }
108
121
  ```
@@ -123,15 +136,19 @@ SpotDown uses a JSON configuration file with the following structure:
123
136
 
124
137
  ## Usage
125
138
 
126
- ### Basic Usage
139
+ ### Starting SpotDown
140
+
141
+ Simply run the following command in your terminal:
127
142
 
128
143
  ```bash
129
- python run.py
144
+ spotdown
130
145
  ```
131
146
 
147
+ The interactive interface will guide you through the download process.
148
+
132
149
  ### Download Individual Songs
133
150
 
134
- 1. Run the script
151
+ 1. Run `spotdown`
135
152
  2. Paste the Spotify song URL when prompted
136
153
  3. The script will automatically:
137
154
  - Extract song information
@@ -140,15 +157,28 @@ python run.py
140
157
 
141
158
  ### Download Playlists
142
159
 
143
- 1. Run the script
160
+ 1. Run `spotdown`
144
161
  2. Paste the Spotify playlist URL when prompted
145
162
  3. All songs in the playlist will be downloaded automatically
146
163
 
164
+ ### Example Usage
165
+
166
+ ```bash
167
+ $ spotdown
168
+ 🎵 Welcome to SpotDown!
169
+ Please paste your Spotify URL: https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh
170
+ 🔍 Processing: Song Name - Artist Name
171
+ ⬇️ Downloading...
172
+ ✅ Download complete!
173
+ ```
174
+
147
175
  ## To Do
148
176
 
149
177
  - [ ] Implement batch download queue
150
178
  - [ ] Add GUI interface option
151
179
  - [ ] Support for additional music platforms
180
+ - [ ] Album art quality selection
181
+ - [ ] Custom output directory configuration
152
182
 
153
183
  ## Disclaimer
154
184
 
@@ -0,0 +1,20 @@
1
+ SpotDown/__init__.py,sha256=ebKRxKh14_cO7UWvOgE4R1RqIFBdekg50xtzc1LU6DU,59
2
+ SpotDown/main.py,sha256=HWhik7Kr1OAnvJPNvaqpO_YqHWCbtyaUIVzaicL3jhg,4999
3
+ SpotDown/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ SpotDown/downloader/youtube_downloader.py,sha256=7Qs1IhSKZqMCm5tuMSCoI-BItGO-28aRLmAdcchcq7w,5492
5
+ SpotDown/extractor/__init__.py,sha256=pQtkjMzB4D8AVEFQ3pnp_UQI-KbYaQhZbKuDnMImN0o,161
6
+ SpotDown/extractor/spotify_extractor.py,sha256=55jndORplGMNb2GiRzo2MkN_v_OrfpEqpKnGjzy6BQo,15130
7
+ SpotDown/extractor/youtube_extractor.py,sha256=v1JDV_NHBpc56UU5kk4tfaRjPB0Jy23_F5r4LE2gOlU,10442
8
+ SpotDown/upload/version.py,sha256=ff4spj6Qs71lt0V8utidpPgidmzTDRCnhGGafOJXvoU,161
9
+ SpotDown/utils/__init__.py,sha256=ZhJJhNQkUd4XG6vkoVQSC3qvSlaArhsRjscn4bfC9WA,128
10
+ SpotDown/utils/config_json.py,sha256=q_9lQhGLmsaZfM1WU-kOMByf8y1Wt9QWnZT5g_IR4Lg,7447
11
+ SpotDown/utils/console_utils.py,sha256=A1zNRyB9yG7BqWzlQgfVZ25cWUjMmYQooRKyeqVlcDw,6823
12
+ SpotDown/utils/file_utils.py,sha256=Lgq_Nu1r-bVIH37H7Mx73WgotN_nrgMUAT2STDyEYoY,3495
13
+ SpotDown/utils/headers.py,sha256=nSF2rEQEl7g2tIJMjz9c5mAQfLiugfWwdlJBhtMfBSo,312
14
+ SpotDown/utils/logger.py,sha256=Bzje-6nwGxL7TzMVhJfhPe7mv-9upYylFvx_zjWJX3M,3290
15
+ spotdown-1.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
+ spotdown-1.0.0.dist-info/METADATA,sha256=DTDziAY2-Bql3Vews8smKT78jQC6qQfXSjFuajbeL1A,5522
17
+ spotdown-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ spotdown-1.0.0.dist-info/entry_points.txt,sha256=qHq6M94aU-XQL0y9R4yFKoqnHjU2nsN53Bj4OuHp3vQ,47
19
+ spotdown-1.0.0.dist-info/top_level.txt,sha256=1E-ZZ8rsrXmBPAsseY_kEfHfNo9w9PtSCaS6pM63-tw,9
20
+ spotdown-1.0.0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- SpotDown/__init__.py,sha256=ebKRxKh14_cO7UWvOgE4R1RqIFBdekg50xtzc1LU6DU,59
2
- SpotDown/main.py,sha256=TjxUMIe0ee8IMbnYhXekO2MoLRiY6pJ5CCPgz7pOpHg,5042
3
- SpotDown/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- SpotDown/downloader/youtube_downloader.py,sha256=zHl98F2Ax72wTg3n8pohmkBZgBIPff0NrD9QKhG-TBM,4589
5
- SpotDown/extractor/__init__.py,sha256=pQtkjMzB4D8AVEFQ3pnp_UQI-KbYaQhZbKuDnMImN0o,161
6
- SpotDown/extractor/spotify_extractor.py,sha256=abE48E4M8s3xf95a28IYD8L4K2-QFDaYoZC5V0Jqu-4,13486
7
- SpotDown/extractor/youtube_extractor.py,sha256=XIPNWEBb5oUkGP1DrQ4h4XTJvHlBlbVIkAFNdpFE-Rc,9586
8
- SpotDown/utils/__init__.py,sha256=ZhJJhNQkUd4XG6vkoVQSC3qvSlaArhsRjscn4bfC9WA,128
9
- SpotDown/utils/config_json.py,sha256=Z_fuM2mYQVPc51sQ_-LXrX-ArTPL-lRDcQ3lLM_hmck,7435
10
- SpotDown/utils/console_utils.py,sha256=A1zNRyB9yG7BqWzlQgfVZ25cWUjMmYQooRKyeqVlcDw,6823
11
- SpotDown/utils/file_utils.py,sha256=Lgq_Nu1r-bVIH37H7Mx73WgotN_nrgMUAT2STDyEYoY,3495
12
- SpotDown/utils/headers.py,sha256=nSF2rEQEl7g2tIJMjz9c5mAQfLiugfWwdlJBhtMfBSo,312
13
- spotdown-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
- spotdown-0.1.0.dist-info/METADATA,sha256=hSndrBHX3Cojvz0QgmbvsxHe8ryhaZRaXrIneggzYuw,5365
15
- spotdown-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- spotdown-0.1.0.dist-info/entry_points.txt,sha256=qHq6M94aU-XQL0y9R4yFKoqnHjU2nsN53Bj4OuHp3vQ,47
17
- spotdown-0.1.0.dist-info/top_level.txt,sha256=1E-ZZ8rsrXmBPAsseY_kEfHfNo9w9PtSCaS6pM63-tw,9
18
- spotdown-0.1.0.dist-info/RECORD,,