SpotDown 0.1.1__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-0.1.1 → spotdown-1.3.0}/MANIFEST.in +3 -3
  2. {spotdown-0.1.1/SpotDown.egg-info → spotdown-1.3.0}/PKG-INFO +63 -30
  3. {spotdown-0.1.1 → spotdown-1.3.0}/README.md +61 -28
  4. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/downloader/youtube_downloader.py +20 -6
  5. spotdown-1.3.0/SpotDown/extractor/spotify_extractor.py +218 -0
  6. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/extractor/youtube_extractor.py +15 -1
  7. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/main.py +6 -8
  8. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/upload/version.py +2 -2
  9. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/config_json.py +2 -2
  10. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/console_utils.py +1 -1
  11. spotdown-1.3.0/SpotDown/utils/ffmpeg_installer.py +374 -0
  12. spotdown-1.3.0/SpotDown/utils/file_utils.py +233 -0
  13. spotdown-1.3.0/SpotDown/utils/logger.py +90 -0
  14. {spotdown-0.1.1 → spotdown-1.3.0/SpotDown.egg-info}/PKG-INFO +63 -30
  15. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/SOURCES.txt +3 -1
  16. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/requires.txt +1 -1
  17. {spotdown-0.1.1 → spotdown-1.3.0}/requirements.txt +2 -2
  18. {spotdown-0.1.1 → spotdown-1.3.0}/setup.py +2 -1
  19. spotdown-0.1.1/SpotDown/extractor/spotify_extractor.py +0 -331
  20. spotdown-0.1.1/SpotDown/utils/file_utils.py +0 -129
  21. {spotdown-0.1.1 → spotdown-1.3.0}/LICENSE +0 -0
  22. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/__init__.py +0 -0
  23. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/downloader/__init__.py +0 -0
  24. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/extractor/__init__.py +0 -0
  25. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/__init__.py +0 -0
  26. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/headers.py +0 -0
  27. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/dependency_links.txt +0 -0
  28. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/entry_points.txt +0 -0
  29. {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/top_level.txt +0 -0
  30. {spotdown-0.1.1 → spotdown-1.3.0}/setup.cfg +0 -0
@@ -1,3 +1,3 @@
1
- include README.md
2
- include requirements.txt
3
- include SpotDown/upload/version.py
1
+ include README.md
2
+ include requirements.txt
3
+ include SpotDown/upload/version.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SpotDown
3
- Version: 0.1.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
@@ -41,6 +41,7 @@ Dynamic: summary
41
41
  ## 💝 Support the Project
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
45
46
 
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)
@@ -50,7 +51,7 @@ Dynamic: summary
50
51
 
51
52
  ---
52
53
 
53
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
54
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
54
55
 
55
56
  </div>
56
57
 
@@ -58,9 +59,9 @@ Dynamic: summary
58
59
 
59
60
  - [✨ Features](#features)
60
61
  - [🛠️ Installation](#️installation)
62
+ - [⚙️ Setup](#setup)
61
63
  - [⚙️ Configuration](#configuration)
62
64
  - [💻 Usage](#usage)
63
- - [⚠️ Disclaimer](#disclaimer)
64
65
 
65
66
  ## Features
66
67
 
@@ -68,38 +69,61 @@ Dynamic: summary
68
69
  - 📋 **Download entire playlists** with ease
69
70
  - 🔍 **No authentication required** - uses web scraping
70
71
  - 🎨 **Automatic cover art embedding** (JPEG format)
72
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
71
73
 
72
74
  ## Installation
73
75
 
74
- ### Prerequisites
76
+ ### Method 1: PyPI (Recommended)
75
77
 
76
- - **Python 3.8+**
77
- - **FFmpeg** (for audio processing)
78
- - **yt-dlp** (for downloading)
78
+ ```bash
79
+ pip install spotdown
80
+ ```
79
81
 
80
- ### 1. Install Python Dependencies
82
+ That's it! You can now run `spotdown` from anywhere in your terminal.
83
+
84
+ ### Method 2: From Source
85
+
86
+ If you prefer to install from source:
81
87
 
82
88
  ```bash
83
- pip install -r requirements.txt
89
+ git clone https://github.com/Arrowar/spotdown.git
90
+ cd spotdown
91
+ pip install -e .
84
92
  ```
85
93
 
86
- ### 2. Install Playwright Chromium
94
+ ### Prerequisites
95
+
96
+ The following dependencies will be automatically installed:
97
+
98
+ - **Python 3.8+**
99
+ - **FFmpeg** (for audio processing)
100
+ - **yt-dlp** (for downloading)
101
+ - **Playwright** (for web scraping)
102
+
103
+ After installation, run this one-time setup command:
87
104
 
88
105
  ```bash
89
106
  playwright install chromium
90
107
  ```
91
108
 
92
- ### 3. Quick Start
109
+ ## Setup
93
110
 
94
- Create a simple launcher script:
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:
95
115
 
96
- ```python
97
- from spotdown.run import main
98
-
99
- if __name__ == "__main__":
100
- main()
116
+ ```
117
+ SPOTIFY_CLIENT_ID=your_client_id_here
118
+ SPOTIPY_CLIENT_SECRET=your_client_secret_here
101
119
  ```
102
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
+
103
127
  ## Configuration
104
128
 
105
129
  SpotDown uses a JSON configuration file with the following structure:
@@ -113,10 +137,6 @@ SpotDown uses a JSON configuration file with the following structure:
113
137
  "DOWNLOAD": {
114
138
  "auto_first": false,
115
139
  "quality": "320K"
116
- },
117
- "BROWSER": {
118
- "headless": true,
119
- "timeout": 6
120
140
  }
121
141
  }
122
142
  ```
@@ -131,21 +151,21 @@ SpotDown uses a JSON configuration file with the following structure:
131
151
  - **`auto_first`**: Automatically select first search result
132
152
  - **`quality`**: Audio quality (320K recommended for best quality)
133
153
 
134
- #### BROWSER Settings
135
- - **`headless`**: Run browser in background (recommended: true)
136
- - **`timeout`**: Browser timeout in seconds
137
-
138
154
  ## Usage
139
155
 
140
- ### Basic Usage
156
+ ### Starting SpotDown
157
+
158
+ Simply run the following command in your terminal:
141
159
 
142
160
  ```bash
143
- python run.py
161
+ spotdown
144
162
  ```
145
163
 
164
+ The interactive interface will guide you through the download process.
165
+
146
166
  ### Download Individual Songs
147
167
 
148
- 1. Run the script
168
+ 1. Run `spotdown`
149
169
  2. Paste the Spotify song URL when prompted
150
170
  3. The script will automatically:
151
171
  - Extract song information
@@ -154,15 +174,28 @@ python run.py
154
174
 
155
175
  ### Download Playlists
156
176
 
157
- 1. Run the script
177
+ 1. Run `spotdown`
158
178
  2. Paste the Spotify playlist URL when prompted
159
179
  3. All songs in the playlist will be downloaded automatically
160
180
 
181
+ ### Example Usage
182
+
183
+ ```bash
184
+ $ spotdown
185
+ 🎵 Welcome to SpotDown!
186
+ Please paste your Spotify URL: https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh
187
+ 🔍 Processing: Song Name - Artist Name
188
+ ⬇️ Downloading...
189
+ ✅ Download complete!
190
+ ```
191
+
161
192
  ## To Do
162
193
 
163
194
  - [ ] Implement batch download queue
164
195
  - [ ] Add GUI interface option
165
196
  - [ ] Support for additional music platforms
197
+ - [ ] Album art quality selection
198
+ - [ ] Custom output directory configuration
166
199
 
167
200
  ## Disclaimer
168
201
 
@@ -9,6 +9,7 @@
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
+
12
13
  ## 🚀 Download & Install
13
14
 
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)
@@ -18,7 +19,7 @@
18
19
 
19
20
  ---
20
21
 
21
- *⚡ **Quick Start:** `pip install spotdown` or download the executable for your platform above*
22
+ *⚡ **Quick Start:** `pip install spotdown && spotdown`*
22
23
 
23
24
  </div>
24
25
 
@@ -26,9 +27,9 @@
26
27
 
27
28
  - [✨ Features](#features)
28
29
  - [🛠️ Installation](#️installation)
30
+ - [⚙️ Setup](#setup)
29
31
  - [⚙️ Configuration](#configuration)
30
32
  - [💻 Usage](#usage)
31
- - [⚠️ Disclaimer](#disclaimer)
32
33
 
33
34
  ## Features
34
35
 
@@ -36,38 +37,61 @@
36
37
  - 📋 **Download entire playlists** with ease
37
38
  - 🔍 **No authentication required** - uses web scraping
38
39
  - 🎨 **Automatic cover art embedding** (JPEG format)
40
+ - ⚡ **Simple command-line interface** - just run `spotdown`!
39
41
 
40
42
  ## Installation
41
43
 
42
- ### Prerequisites
44
+ ### Method 1: PyPI (Recommended)
43
45
 
44
- - **Python 3.8+**
45
- - **FFmpeg** (for audio processing)
46
- - **yt-dlp** (for downloading)
46
+ ```bash
47
+ pip install spotdown
48
+ ```
47
49
 
48
- ### 1. Install Python Dependencies
50
+ That's it! You can now run `spotdown` from anywhere in your terminal.
51
+
52
+ ### Method 2: From Source
53
+
54
+ If you prefer to install from source:
49
55
 
50
56
  ```bash
51
- pip install -r requirements.txt
57
+ git clone https://github.com/Arrowar/spotdown.git
58
+ cd spotdown
59
+ pip install -e .
52
60
  ```
53
61
 
54
- ### 2. Install Playwright Chromium
62
+ ### Prerequisites
63
+
64
+ The following dependencies will be automatically installed:
65
+
66
+ - **Python 3.8+**
67
+ - **FFmpeg** (for audio processing)
68
+ - **yt-dlp** (for downloading)
69
+ - **Playwright** (for web scraping)
70
+
71
+ After installation, run this one-time setup command:
55
72
 
56
73
  ```bash
57
74
  playwright install chromium
58
75
  ```
59
76
 
60
- ### 3. Quick Start
77
+ ## Setup
61
78
 
62
- Create a simple launcher script:
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:
63
83
 
64
- ```python
65
- from spotdown.run import main
66
-
67
- if __name__ == "__main__":
68
- main()
84
+ ```
85
+ SPOTIFY_CLIENT_ID=your_client_id_here
86
+ SPOTIPY_CLIENT_SECRET=your_client_secret_here
69
87
  ```
70
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
+
71
95
  ## Configuration
72
96
 
73
97
  SpotDown uses a JSON configuration file with the following structure:
@@ -81,10 +105,6 @@ SpotDown uses a JSON configuration file with the following structure:
81
105
  "DOWNLOAD": {
82
106
  "auto_first": false,
83
107
  "quality": "320K"
84
- },
85
- "BROWSER": {
86
- "headless": true,
87
- "timeout": 6
88
108
  }
89
109
  }
90
110
  ```
@@ -99,21 +119,21 @@ SpotDown uses a JSON configuration file with the following structure:
99
119
  - **`auto_first`**: Automatically select first search result
100
120
  - **`quality`**: Audio quality (320K recommended for best quality)
101
121
 
102
- #### BROWSER Settings
103
- - **`headless`**: Run browser in background (recommended: true)
104
- - **`timeout`**: Browser timeout in seconds
105
-
106
122
  ## Usage
107
123
 
108
- ### Basic Usage
124
+ ### Starting SpotDown
125
+
126
+ Simply run the following command in your terminal:
109
127
 
110
128
  ```bash
111
- python run.py
129
+ spotdown
112
130
  ```
113
131
 
132
+ The interactive interface will guide you through the download process.
133
+
114
134
  ### Download Individual Songs
115
135
 
116
- 1. Run the script
136
+ 1. Run `spotdown`
117
137
  2. Paste the Spotify song URL when prompted
118
138
  3. The script will automatically:
119
139
  - Extract song information
@@ -122,15 +142,28 @@ python run.py
122
142
 
123
143
  ### Download Playlists
124
144
 
125
- 1. Run the script
145
+ 1. Run `spotdown`
126
146
  2. Paste the Spotify playlist URL when prompted
127
147
  3. All songs in the playlist will be downloaded automatically
128
148
 
149
+ ### Example Usage
150
+
151
+ ```bash
152
+ $ spotdown
153
+ 🎵 Welcome to SpotDown!
154
+ Please paste your Spotify URL: https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh
155
+ 🔍 Processing: Song Name - Artist Name
156
+ ⬇️ Downloading...
157
+ ✅ Download complete!
158
+ ```
159
+
129
160
  ## To Do
130
161
 
131
162
  - [ ] Implement batch download queue
132
163
  - [ ] Add GUI interface option
133
164
  - [ ] Support for additional music platforms
165
+ - [ ] Album art quality selection
166
+ - [ ] Custom output directory configuration
134
167
 
135
168
  ## Disclaimer
136
169
 
@@ -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
 
@@ -14,7 +15,7 @@ from rich.console import Console
14
15
 
15
16
  # Internal utils
16
17
  from SpotDown.utils.config_json import config_manager
17
- from SpotDown.utils.file_utils import FileUtils
18
+ from SpotDown.utils.file_utils import file_utils
18
19
 
19
20
 
20
21
  # Variable
@@ -24,7 +25,7 @@ quality = config_manager.get("DOWNLOAD", "quality")
24
25
  class YouTubeDownloader:
25
26
  def __init__(self):
26
27
  self.console = Console()
27
- self.file_utils = FileUtils()
28
+ self.file_utils = file_utils
28
29
 
29
30
  def download(self, video_info: Dict, spotify_info: Dict) -> bool:
30
31
  """
@@ -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 = [
@@ -82,6 +87,7 @@ class YouTubeDownloader:
82
87
  '--no-playlist',
83
88
  '--embed-metadata',
84
89
  '--add-metadata',
90
+ '--ffmpeg-location', self.file_utils.ffmpeg_path
85
91
  ]
86
92
 
87
93
  if cover_path and cover_path.exists():
@@ -94,6 +100,7 @@ class YouTubeDownloader:
94
100
  console=self.console
95
101
  ) as progress:
96
102
  task = progress.add_task("Downloading...", total=None)
103
+ logging.info(f"Running yt-dlp with options: {ytdlp_options}")
97
104
  process = subprocess.run(
98
105
  ytdlp_options,
99
106
  capture_output=True,
@@ -102,30 +109,37 @@ class YouTubeDownloader:
102
109
  progress.remove_task(task)
103
110
 
104
111
  if process.returncode == 0:
112
+ logging.info("yt-dlp finished successfully")
105
113
 
106
114
  # Find the downloaded file
107
115
  downloaded_files = list(music_folder.glob(f"{filename}.*"))
108
116
  if downloaded_files:
109
117
  self.console.print("[red]Download completed![/red]")
118
+ logging.info(f"Download completed: {downloaded_files[0]}")
110
119
 
111
120
  # Remove cover file after embedding
112
121
  if cover_path and cover_path.exists():
113
122
  try:
114
123
  cover_path.unlink()
115
- except Exception:
116
- pass
117
-
124
+ logging.info(f"Removed temporary cover file: {cover_path}")
125
+
126
+ except Exception as ex:
127
+ logging.warning(f"Failed to remove cover file: {ex}")
128
+
118
129
  return True
119
130
 
120
131
  else:
121
132
  self.console.print("[yellow]Download apparently succeeded but file not found[/yellow]")
133
+ logging.error("Download apparently succeeded but file not found")
122
134
  return False
123
-
135
+
124
136
  else:
125
137
  self.console.print("[red]Download error:[/red]")
126
138
  self.console.print(f"[red]{process.stderr}[/red]")
139
+ logging.error(f"yt-dlp error: {process.stderr}")
127
140
  return False
128
141
 
129
142
  except Exception as e:
130
143
  self.console.print(f"[red]Error during download: {e}[/red]")
144
+ logging.error(f"Error during download: {e}")
131
145
  return False
@@ -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 []