SpotDown 0.1.0__tar.gz → 1.0.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 (28) hide show
  1. spotdown-1.0.0/MANIFEST.in +3 -0
  2. {spotdown-0.1.0 → spotdown-1.0.0}/PKG-INFO +54 -33
  3. {spotdown-0.1.0 → spotdown-1.0.0}/README.md +44 -28
  4. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/downloader/youtube_downloader.py +17 -4
  5. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/extractor/spotify_extractor.py +24 -0
  6. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/extractor/youtube_extractor.py +15 -1
  7. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/main.py +4 -8
  8. spotdown-1.0.0/SpotDown/upload/version.py +5 -0
  9. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/utils/config_json.py +2 -2
  10. spotdown-1.0.0/SpotDown/utils/logger.py +90 -0
  11. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/PKG-INFO +54 -33
  12. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/SOURCES.txt +5 -1
  13. spotdown-1.0.0/requirements.txt +8 -0
  14. spotdown-1.0.0/setup.py +51 -0
  15. spotdown-0.1.0/setup.py +0 -39
  16. {spotdown-0.1.0 → spotdown-1.0.0}/LICENSE +0 -0
  17. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/__init__.py +0 -0
  18. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/downloader/__init__.py +0 -0
  19. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/extractor/__init__.py +0 -0
  20. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/utils/__init__.py +0 -0
  21. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/utils/console_utils.py +0 -0
  22. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/utils/file_utils.py +0 -0
  23. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown/utils/headers.py +0 -0
  24. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/dependency_links.txt +0 -0
  25. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/entry_points.txt +0 -0
  26. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/requires.txt +0 -0
  27. {spotdown-0.1.0 → spotdown-1.0.0}/SpotDown.egg-info/top_level.txt +0 -0
  28. {spotdown-0.1.0 → spotdown-1.0.0}/setup.cfg +0 -0
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include requirements.txt
3
+ include SpotDown/upload/version.py
@@ -1,10 +1,13 @@
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
@@ -17,13 +20,15 @@ Requires-Dist: unidecode
17
20
  Requires-Dist: yt-dlp
18
21
  Requires-Dist: Pillow
19
22
  Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
20
25
  Dynamic: description
21
26
  Dynamic: description-content-type
22
27
  Dynamic: home-page
23
28
  Dynamic: license-file
24
- Dynamic: project-url
25
29
  Dynamic: requires-dist
26
30
  Dynamic: requires-python
31
+ Dynamic: summary
27
32
 
28
33
  <div align="center">
29
34
 
@@ -36,16 +41,10 @@ Dynamic: requires-python
36
41
  ## 💝 Support the Project
37
42
 
38
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)
39
- ## 🚀 Download & Install
40
-
41
- [![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)
42
- [![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)
43
- [![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)
44
- [![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)
45
44
 
46
45
  ---
47
46
 
48
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
47
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
49
48
 
50
49
  </div>
51
50
 
@@ -63,36 +62,41 @@ Dynamic: requires-python
63
62
  - 📋 **Download entire playlists** with ease
64
63
  - 🔍 **No authentication required** - uses web scraping
65
64
  - 🎨 **Automatic cover art embedding** (JPEG format)
65
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
66
66
 
67
67
  ## Installation
68
68
 
69
- ### Prerequisites
70
-
71
- - **Python 3.8+**
72
- - **FFmpeg** (for audio processing)
73
- - **yt-dlp** (for downloading)
74
-
75
- ### 1. Install Python Dependencies
69
+ ### Method 1: PyPI (Recommended)
76
70
 
77
71
  ```bash
78
- pip install -r requirements.txt
72
+ pip install spotdown
79
73
  ```
80
74
 
81
- ### 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:
82
80
 
83
81
  ```bash
84
- playwright install chromium
82
+ git clone https://github.com/Arrowar/spotdown.git
83
+ cd spotdown
84
+ pip install -e .
85
85
  ```
86
86
 
87
- ### 3. Quick Start
87
+ ### Prerequisites
88
88
 
89
- Create a simple launcher script:
89
+ The following dependencies will be automatically installed:
90
90
 
91
- ```python
92
- 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:
93
97
 
94
- if __name__ == "__main__":
95
- main()
98
+ ```bash
99
+ playwright install chromium
96
100
  ```
97
101
 
98
102
  ## Configuration
@@ -111,7 +115,7 @@ SpotDown uses a JSON configuration file with the following structure:
111
115
  },
112
116
  "BROWSER": {
113
117
  "headless": true,
114
- "timeout": 6
118
+ "timeout": 8
115
119
  }
116
120
  }
117
121
  ```
@@ -132,15 +136,19 @@ SpotDown uses a JSON configuration file with the following structure:
132
136
 
133
137
  ## Usage
134
138
 
135
- ### Basic Usage
139
+ ### Starting SpotDown
140
+
141
+ Simply run the following command in your terminal:
136
142
 
137
143
  ```bash
138
- python run.py
144
+ spotdown
139
145
  ```
140
146
 
147
+ The interactive interface will guide you through the download process.
148
+
141
149
  ### Download Individual Songs
142
150
 
143
- 1. Run the script
151
+ 1. Run `spotdown`
144
152
  2. Paste the Spotify song URL when prompted
145
153
  3. The script will automatically:
146
154
  - Extract song information
@@ -149,15 +157,28 @@ python run.py
149
157
 
150
158
  ### Download Playlists
151
159
 
152
- 1. Run the script
160
+ 1. Run `spotdown`
153
161
  2. Paste the Spotify playlist URL when prompted
154
162
  3. All songs in the playlist will be downloaded automatically
155
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
+
156
175
  ## To Do
157
176
 
158
177
  - [ ] Implement batch download queue
159
178
  - [ ] Add GUI interface option
160
179
  - [ ] Support for additional music platforms
180
+ - [ ] Album art quality selection
181
+ - [ ] Custom output directory configuration
161
182
 
162
183
  ## Disclaimer
163
184
 
@@ -9,16 +9,10 @@
9
9
  ## 💝 Support the Project
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
- ## 🚀 Download & Install
13
-
14
- [![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)
15
- [![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)
16
- [![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)
17
- [![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)
18
12
 
19
13
  ---
20
14
 
21
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
15
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
22
16
 
23
17
  </div>
24
18
 
@@ -36,36 +30,41 @@
36
30
  - 📋 **Download entire playlists** with ease
37
31
  - 🔍 **No authentication required** - uses web scraping
38
32
  - 🎨 **Automatic cover art embedding** (JPEG format)
33
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
39
34
 
40
35
  ## Installation
41
36
 
42
- ### Prerequisites
43
-
44
- - **Python 3.8+**
45
- - **FFmpeg** (for audio processing)
46
- - **yt-dlp** (for downloading)
47
-
48
- ### 1. Install Python Dependencies
37
+ ### Method 1: PyPI (Recommended)
49
38
 
50
39
  ```bash
51
- pip install -r requirements.txt
40
+ pip install spotdown
52
41
  ```
53
42
 
54
- ### 2. Install Playwright Chromium
43
+ That's it! You can now run `spotdown` from anywhere in your terminal.
44
+
45
+ ### Method 2: From Source
46
+
47
+ If you prefer to install from source:
55
48
 
56
49
  ```bash
57
- playwright install chromium
50
+ git clone https://github.com/Arrowar/spotdown.git
51
+ cd spotdown
52
+ pip install -e .
58
53
  ```
59
54
 
60
- ### 3. Quick Start
55
+ ### Prerequisites
61
56
 
62
- Create a simple launcher script:
57
+ The following dependencies will be automatically installed:
63
58
 
64
- ```python
65
- from spotdown.run import main
59
+ - **Python 3.8+**
60
+ - **FFmpeg** (for audio processing)
61
+ - **yt-dlp** (for downloading)
62
+ - **Playwright** (for web scraping)
63
+
64
+ After installation, run this one-time setup command:
66
65
 
67
- if __name__ == "__main__":
68
- main()
66
+ ```bash
67
+ playwright install chromium
69
68
  ```
70
69
 
71
70
  ## Configuration
@@ -84,7 +83,7 @@ SpotDown uses a JSON configuration file with the following structure:
84
83
  },
85
84
  "BROWSER": {
86
85
  "headless": true,
87
- "timeout": 6
86
+ "timeout": 8
88
87
  }
89
88
  }
90
89
  ```
@@ -105,15 +104,19 @@ SpotDown uses a JSON configuration file with the following structure:
105
104
 
106
105
  ## Usage
107
106
 
108
- ### Basic Usage
107
+ ### Starting SpotDown
108
+
109
+ Simply run the following command in your terminal:
109
110
 
110
111
  ```bash
111
- python run.py
112
+ spotdown
112
113
  ```
113
114
 
115
+ The interactive interface will guide you through the download process.
116
+
114
117
  ### Download Individual Songs
115
118
 
116
- 1. Run the script
119
+ 1. Run `spotdown`
117
120
  2. Paste the Spotify song URL when prompted
118
121
  3. The script will automatically:
119
122
  - Extract song information
@@ -122,15 +125,28 @@ python run.py
122
125
 
123
126
  ### Download Playlists
124
127
 
125
- 1. Run the script
128
+ 1. Run `spotdown`
126
129
  2. Paste the Spotify playlist URL when prompted
127
130
  3. All songs in the playlist will be downloaded automatically
128
131
 
132
+ ### Example Usage
133
+
134
+ ```bash
135
+ $ spotdown
136
+ 🎵 Welcome to SpotDown!
137
+ Please paste your Spotify URL: https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh
138
+ 🔍 Processing: Song Name - Artist Name
139
+ ⬇️ Downloading...
140
+ ✅ Download complete!
141
+ ```
142
+
129
143
  ## To Do
130
144
 
131
145
  - [ ] Implement batch download queue
132
146
  - [ ] Add GUI interface option
133
147
  - [ ] Support for additional music platforms
148
+ - [ ] Album art quality selection
149
+ - [ ] Custom output directory configuration
134
150
 
135
151
  ## Disclaimer
136
152
 
@@ -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
 
@@ -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,10 +1,13 @@
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
@@ -17,13 +20,15 @@ Requires-Dist: unidecode
17
20
  Requires-Dist: yt-dlp
18
21
  Requires-Dist: Pillow
19
22
  Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
20
25
  Dynamic: description
21
26
  Dynamic: description-content-type
22
27
  Dynamic: home-page
23
28
  Dynamic: license-file
24
- Dynamic: project-url
25
29
  Dynamic: requires-dist
26
30
  Dynamic: requires-python
31
+ Dynamic: summary
27
32
 
28
33
  <div align="center">
29
34
 
@@ -36,16 +41,10 @@ Dynamic: requires-python
36
41
  ## 💝 Support the Project
37
42
 
38
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)
39
- ## 🚀 Download & Install
40
-
41
- [![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)
42
- [![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)
43
- [![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)
44
- [![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)
45
44
 
46
45
  ---
47
46
 
48
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
47
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
49
48
 
50
49
  </div>
51
50
 
@@ -63,36 +62,41 @@ Dynamic: requires-python
63
62
  - 📋 **Download entire playlists** with ease
64
63
  - 🔍 **No authentication required** - uses web scraping
65
64
  - 🎨 **Automatic cover art embedding** (JPEG format)
65
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
66
66
 
67
67
  ## Installation
68
68
 
69
- ### Prerequisites
70
-
71
- - **Python 3.8+**
72
- - **FFmpeg** (for audio processing)
73
- - **yt-dlp** (for downloading)
74
-
75
- ### 1. Install Python Dependencies
69
+ ### Method 1: PyPI (Recommended)
76
70
 
77
71
  ```bash
78
- pip install -r requirements.txt
72
+ pip install spotdown
79
73
  ```
80
74
 
81
- ### 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:
82
80
 
83
81
  ```bash
84
- playwright install chromium
82
+ git clone https://github.com/Arrowar/spotdown.git
83
+ cd spotdown
84
+ pip install -e .
85
85
  ```
86
86
 
87
- ### 3. Quick Start
87
+ ### Prerequisites
88
88
 
89
- Create a simple launcher script:
89
+ The following dependencies will be automatically installed:
90
90
 
91
- ```python
92
- 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:
93
97
 
94
- if __name__ == "__main__":
95
- main()
98
+ ```bash
99
+ playwright install chromium
96
100
  ```
97
101
 
98
102
  ## Configuration
@@ -111,7 +115,7 @@ SpotDown uses a JSON configuration file with the following structure:
111
115
  },
112
116
  "BROWSER": {
113
117
  "headless": true,
114
- "timeout": 6
118
+ "timeout": 8
115
119
  }
116
120
  }
117
121
  ```
@@ -132,15 +136,19 @@ SpotDown uses a JSON configuration file with the following structure:
132
136
 
133
137
  ## Usage
134
138
 
135
- ### Basic Usage
139
+ ### Starting SpotDown
140
+
141
+ Simply run the following command in your terminal:
136
142
 
137
143
  ```bash
138
- python run.py
144
+ spotdown
139
145
  ```
140
146
 
147
+ The interactive interface will guide you through the download process.
148
+
141
149
  ### Download Individual Songs
142
150
 
143
- 1. Run the script
151
+ 1. Run `spotdown`
144
152
  2. Paste the Spotify song URL when prompted
145
153
  3. The script will automatically:
146
154
  - Extract song information
@@ -149,15 +157,28 @@ python run.py
149
157
 
150
158
  ### Download Playlists
151
159
 
152
- 1. Run the script
160
+ 1. Run `spotdown`
153
161
  2. Paste the Spotify playlist URL when prompted
154
162
  3. All songs in the playlist will be downloaded automatically
155
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
+
156
175
  ## To Do
157
176
 
158
177
  - [ ] Implement batch download queue
159
178
  - [ ] Add GUI interface option
160
179
  - [ ] Support for additional music platforms
180
+ - [ ] Album art quality selection
181
+ - [ ] Custom output directory configuration
161
182
 
162
183
  ## Disclaimer
163
184
 
@@ -1,5 +1,7 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
4
+ requirements.txt
3
5
  setup.py
4
6
  SpotDown/__init__.py
5
7
  SpotDown/main.py
@@ -14,8 +16,10 @@ SpotDown/downloader/youtube_downloader.py
14
16
  SpotDown/extractor/__init__.py
15
17
  SpotDown/extractor/spotify_extractor.py
16
18
  SpotDown/extractor/youtube_extractor.py
19
+ SpotDown/upload/version.py
17
20
  SpotDown/utils/__init__.py
18
21
  SpotDown/utils/config_json.py
19
22
  SpotDown/utils/console_utils.py
20
23
  SpotDown/utils/file_utils.py
21
- SpotDown/utils/headers.py
24
+ SpotDown/utils/headers.py
25
+ SpotDown/utils/logger.py
@@ -0,0 +1,8 @@
1
+ rich
2
+ httpx
3
+ playwright
4
+ unidecode
5
+ ua-generator
6
+ unidecode
7
+ yt-dlp
8
+ Pillow
@@ -0,0 +1,51 @@
1
+ # 05.04.2024
2
+
3
+ from pathlib import Path
4
+ from setuptools import setup, find_packages
5
+
6
+ base_dir = Path(__file__).resolve().parent
7
+
8
+
9
+ # Read version
10
+ version = {}
11
+ version_file = base_dir / "SpotDown" / "upload" / "version.py"
12
+ with open(version_file, encoding="utf-8") as f:
13
+ exec(f.read(), version)
14
+
15
+
16
+ # Read requirements
17
+ requirements_file = base_dir / "requirements.txt"
18
+ with open(requirements_file, encoding="utf-8") as f:
19
+ install_requires = f.read().splitlines()
20
+
21
+
22
+ # Read README.md
23
+ readme_file = base_dir / "README.md"
24
+ with open(readme_file, encoding="utf-8") as f:
25
+ long_description = f.read()
26
+
27
+
28
+ setup(
29
+ name="SpotDown",
30
+ version=version["__version__"],
31
+ author="Arrowar",
32
+ author_email="author@example.com",
33
+ description="A command-line program to download music",
34
+ long_description=long_description,
35
+ long_description_content_type="text/markdown",
36
+ url="https://github.com/Arrowar/spotdown",
37
+ packages=find_packages(),
38
+ include_package_data=True,
39
+ install_requires=install_requires,
40
+ entry_points={
41
+ "console_scripts": [
42
+ "spotdown=SpotDown.main:run",
43
+ ],
44
+ },
45
+ classifiers=[
46
+ "Programming Language :: Python :: 3",
47
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
48
+ "Operating System :: OS Independent",
49
+ ],
50
+ python_requires=">=3.8",
51
+ )
spotdown-0.1.0/setup.py DELETED
@@ -1,39 +0,0 @@
1
- import os
2
- import re
3
- from setuptools import setup, find_packages
4
-
5
- def read_readme():
6
- with open("README.md", "r", encoding="utf-8") as fh:
7
- return fh.read()
8
-
9
- requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
10
- if os.path.exists(requirements_path):
11
- with open(requirements_path, "r", encoding="utf-8-sig") as f:
12
- required_packages = f.read().splitlines()
13
- else:
14
- required_packages = []
15
-
16
- def get_version():
17
- return "0.1.0"
18
-
19
- setup(
20
- name="SpotDown",
21
- version=get_version(),
22
- long_description=read_readme(),
23
- long_description_content_type="text/markdown",
24
- author="Arrowar",
25
- url="https://github.com/Arrowar/SpotDown",
26
- packages=find_packages(include=["SpotDown", "SpotDown.*"]),
27
- install_requires=required_packages,
28
- python_requires='>=3.8',
29
- entry_points={
30
- "console_scripts": [
31
- "spotdown=SpotDown.main:run",
32
- ],
33
- },
34
- include_package_data=True,
35
- project_urls={
36
- "Bug Reports": "https://github.com/Arrowar/SpotDown/issues",
37
- "Source": "https://github.com/Arrowar/SpotDown"
38
- }
39
- )
File without changes
File without changes
File without changes