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.
- {spotdown-0.1.1 → spotdown-1.3.0}/MANIFEST.in +3 -3
- {spotdown-0.1.1/SpotDown.egg-info → spotdown-1.3.0}/PKG-INFO +63 -30
- {spotdown-0.1.1 → spotdown-1.3.0}/README.md +61 -28
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/downloader/youtube_downloader.py +20 -6
- spotdown-1.3.0/SpotDown/extractor/spotify_extractor.py +218 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/extractor/youtube_extractor.py +15 -1
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/main.py +6 -8
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/upload/version.py +2 -2
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/config_json.py +2 -2
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/console_utils.py +1 -1
- spotdown-1.3.0/SpotDown/utils/ffmpeg_installer.py +374 -0
- spotdown-1.3.0/SpotDown/utils/file_utils.py +233 -0
- spotdown-1.3.0/SpotDown/utils/logger.py +90 -0
- {spotdown-0.1.1 → spotdown-1.3.0/SpotDown.egg-info}/PKG-INFO +63 -30
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/SOURCES.txt +3 -1
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/requires.txt +1 -1
- {spotdown-0.1.1 → spotdown-1.3.0}/requirements.txt +2 -2
- {spotdown-0.1.1 → spotdown-1.3.0}/setup.py +2 -1
- spotdown-0.1.1/SpotDown/extractor/spotify_extractor.py +0 -331
- spotdown-0.1.1/SpotDown/utils/file_utils.py +0 -129
- {spotdown-0.1.1 → spotdown-1.3.0}/LICENSE +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/__init__.py +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/downloader/__init__.py +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/extractor/__init__.py +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/__init__.py +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown/utils/headers.py +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/dependency_links.txt +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/entry_points.txt +0 -0
- {spotdown-0.1.1 → spotdown-1.3.0}/SpotDown.egg-info/top_level.txt +0 -0
- {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:
|
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
|
[](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
|
44
|
+
|
44
45
|
## 🚀 Download & Install
|
45
46
|
|
46
47
|
[](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
|
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
|
-
###
|
76
|
+
### Method 1: PyPI (Recommended)
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
```bash
|
79
|
+
pip install spotdown
|
80
|
+
```
|
79
81
|
|
80
|
-
|
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
|
-
|
89
|
+
git clone https://github.com/Arrowar/spotdown.git
|
90
|
+
cd spotdown
|
91
|
+
pip install -e .
|
84
92
|
```
|
85
93
|
|
86
|
-
###
|
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
|
-
|
109
|
+
## Setup
|
93
110
|
|
94
|
-
|
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
|
-
```
|
97
|
-
|
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
|
-
###
|
156
|
+
### Starting SpotDown
|
157
|
+
|
158
|
+
Simply run the following command in your terminal:
|
141
159
|
|
142
160
|
```bash
|
143
|
-
|
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
|
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
|
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
|
[](https://www.paypal.com/donate/?hosted_button_id=UXTWMT8P6HE2C)
|
12
|
+
|
12
13
|
## 🚀 Download & Install
|
13
14
|
|
14
15
|
[](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
|
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
|
-
###
|
44
|
+
### Method 1: PyPI (Recommended)
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
46
|
+
```bash
|
47
|
+
pip install spotdown
|
48
|
+
```
|
47
49
|
|
48
|
-
|
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
|
-
|
57
|
+
git clone https://github.com/Arrowar/spotdown.git
|
58
|
+
cd spotdown
|
59
|
+
pip install -e .
|
52
60
|
```
|
53
61
|
|
54
|
-
###
|
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
|
-
|
77
|
+
## Setup
|
61
78
|
|
62
|
-
|
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
|
-
```
|
65
|
-
|
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
|
-
###
|
124
|
+
### Starting SpotDown
|
125
|
+
|
126
|
+
Simply run the following command in your terminal:
|
109
127
|
|
110
128
|
```bash
|
111
|
-
|
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
|
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
|
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
|
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 =
|
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
|
-
|
116
|
-
|
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 []
|