SpotDown 0.0.1__py3-none-any.whl → 0.0.8__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.
- SpotDown/__init__.py +0 -0
- SpotDown/downloader/__init__.py +0 -0
- SpotDown/downloader/youtube_downloader.py +131 -0
- SpotDown/extractor/__init__.py +6 -0
- SpotDown/extractor/spotify_extractor.py +331 -0
- SpotDown/extractor/youtube_extractor.py +271 -0
- SpotDown/main.py +139 -0
- SpotDown/utils/__init__.py +6 -0
- SpotDown/utils/config_json.py +223 -0
- SpotDown/utils/console_utils.py +188 -0
- SpotDown/utils/file_utils.py +129 -0
- SpotDown/utils/headers.py +18 -0
- {spotdown-0.0.1.dist-info → spotdown-0.0.8.dist-info}/METADATA +180 -182
- spotdown-0.0.8.dist-info/RECORD +18 -0
- spotdown-0.0.8.dist-info/entry_points.txt +2 -0
- {spotdown-0.0.1.dist-info → spotdown-0.0.8.dist-info}/licenses/LICENSE +674 -674
- spotdown-0.0.8.dist-info/top_level.txt +1 -0
- spotdown-0.0.1.dist-info/RECORD +0 -6
- spotdown-0.0.1.dist-info/entry_points.txt +0 -2
- spotdown-0.0.1.dist-info/top_level.txt +0 -1
- {spotdown-0.0.1.dist-info → spotdown-0.0.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,188 @@
|
|
1
|
+
# 05.04.2024
|
2
|
+
|
3
|
+
import os
|
4
|
+
import platform
|
5
|
+
from typing import Dict, List
|
6
|
+
|
7
|
+
|
8
|
+
# External imports
|
9
|
+
from rich.console import Console
|
10
|
+
from rich.prompt import Prompt
|
11
|
+
|
12
|
+
|
13
|
+
# Internal utils
|
14
|
+
from SpotDown.utils.config_json import config_manager
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
# Variable
|
19
|
+
CLEAN_CONSOLE = config_manager.get('DEFAULT', 'clean_console')
|
20
|
+
SHOW_MESSAGE = config_manager.get('DEFAULT', 'show_message')
|
21
|
+
AUTO_FIRST = config_manager.get('DOWNLOAD', 'auto_first')
|
22
|
+
|
23
|
+
|
24
|
+
class ConsoleUtils:
|
25
|
+
def __init__(self):
|
26
|
+
self.console = Console()
|
27
|
+
|
28
|
+
def display_spotify_info(self, spotify_info: Dict):
|
29
|
+
"""
|
30
|
+
Display Spotify information
|
31
|
+
|
32
|
+
Args:
|
33
|
+
spotify_info (Dict): Spotify track data
|
34
|
+
"""
|
35
|
+
self.console.print("[bold green]Spotify Track Information: [/bold green]")
|
36
|
+
self.console.print(f"[cyan]Title: [red]{spotify_info['title']}[/red]")
|
37
|
+
self.console.print(f"[cyan]Artist: [red]{spotify_info['artist']}[/red]")
|
38
|
+
|
39
|
+
if spotify_info.get('album'):
|
40
|
+
self.console.print(f"[cyan]Album: [red]{spotify_info['album']}[/red]")
|
41
|
+
if spotify_info.get('year'):
|
42
|
+
self.console.print(f"[cyan]Year: [red]{spotify_info['year']}[/red]")
|
43
|
+
if spotify_info.get('duration_formatted'):
|
44
|
+
self.console.print(f"[cyan]Duration: [red]{spotify_info['duration_formatted']}[/red]")
|
45
|
+
if spotify_info.get('label'):
|
46
|
+
self.console.print(f"[cyan]Label: [red]{spotify_info['label']}[/red]")
|
47
|
+
|
48
|
+
def display_youtube_results(self, youtube_results: List[Dict]):
|
49
|
+
"""
|
50
|
+
Display YouTube results
|
51
|
+
|
52
|
+
Args:
|
53
|
+
youtube_results (List[Dict]): List of YouTube videos
|
54
|
+
"""
|
55
|
+
if not youtube_results:
|
56
|
+
self.console.print("[red]No results found")
|
57
|
+
return
|
58
|
+
|
59
|
+
for i, video in enumerate(youtube_results, 1):
|
60
|
+
self.console.print(f"[green]{i}. {video['title']}[/green], Channel: [cyan]{video['channel']}[/cyan], Duration: [red]{video['duration_formatted']}[/red], Difference: [yellow]{video.get('duration_difference', 'N/A')}s[/yellow]")
|
61
|
+
|
62
|
+
def show_download_menu(self, results_count: int):
|
63
|
+
"""
|
64
|
+
Show download selection menu
|
65
|
+
|
66
|
+
Args:
|
67
|
+
results_count (int): Number of available results
|
68
|
+
"""
|
69
|
+
self.console.print("\n[bold cyan]Download selection: [/bold cyan]")
|
70
|
+
self.console.print("[dim]Options:[/dim]")
|
71
|
+
self.console.print(f"• [green]1-{results_count}[/green]: Download the corresponding video")
|
72
|
+
self.console.print("• [yellow]ENTER[/yellow]: Automatically download the first (most similar)")
|
73
|
+
self.console.print("• [red]0[/red]: Exit without downloading")
|
74
|
+
|
75
|
+
def get_download_choice(self, max_results: int) -> int:
|
76
|
+
"""
|
77
|
+
Get user choice for download
|
78
|
+
|
79
|
+
Args:
|
80
|
+
max_results (int): Maximum number of results
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
int: User choice (0 = exit, 1-n = video)
|
84
|
+
"""
|
85
|
+
if AUTO_FIRST:
|
86
|
+
return 1
|
87
|
+
|
88
|
+
while True:
|
89
|
+
try:
|
90
|
+
choice = Prompt.ask(
|
91
|
+
"\n[bold purple]Which video do you want to download?[/bold purple]",
|
92
|
+
default="1"
|
93
|
+
).strip()
|
94
|
+
|
95
|
+
if choice == "0":
|
96
|
+
return 0
|
97
|
+
|
98
|
+
# Default: first video
|
99
|
+
if choice == "" or choice == "1":
|
100
|
+
return 1
|
101
|
+
|
102
|
+
# Check for valid number
|
103
|
+
choice_num = int(choice)
|
104
|
+
if 1 <= choice_num <= max_results:
|
105
|
+
return choice_num
|
106
|
+
else:
|
107
|
+
self.console.print(f"[red]Invalid choice. Enter a number between 1 and {max_results}[/red]")
|
108
|
+
continue
|
109
|
+
|
110
|
+
except ValueError:
|
111
|
+
self.console.print("[red]Enter a valid number[/red]")
|
112
|
+
continue
|
113
|
+
|
114
|
+
def show_download_info(self, music_folder, filename: str):
|
115
|
+
"""
|
116
|
+
Show download information
|
117
|
+
|
118
|
+
Args:
|
119
|
+
music_folder: Destination folder
|
120
|
+
filename (str): File name
|
121
|
+
"""
|
122
|
+
self.console.print(f"[blue]File name: {filename}[/blue]")
|
123
|
+
self.console.print(f"[blue]Destination folder: {music_folder}[/blue]")
|
124
|
+
|
125
|
+
def show_download_start(self, video_title: str, video_url: str):
|
126
|
+
"""
|
127
|
+
Show download start
|
128
|
+
|
129
|
+
Args:
|
130
|
+
video_title (str): Video title
|
131
|
+
video_url (str): Video URL
|
132
|
+
"""
|
133
|
+
self.console.print(f"[yellow]\nDownloading: {video_title}[/yellow]")
|
134
|
+
self.console.print(f"[dim]URL: {video_url}[/dim]")
|
135
|
+
|
136
|
+
def show_success(self, message: str):
|
137
|
+
"""Show success message"""
|
138
|
+
self.console.print(f"[green]{message}[/green]")
|
139
|
+
|
140
|
+
def show_error(self, message: str):
|
141
|
+
"""Show error message"""
|
142
|
+
self.console.print(f"[red]{message}[/red]")
|
143
|
+
|
144
|
+
def show_warning(self, message: str):
|
145
|
+
"""Show warning message"""
|
146
|
+
self.console.print(f"[yellow]{message}[/yellow]")
|
147
|
+
|
148
|
+
def show_info(self, message: str):
|
149
|
+
"""Show informational message"""
|
150
|
+
self.console.print(f"[blue]{message}[/blue]")
|
151
|
+
|
152
|
+
def get_spotify_url(self) -> str:
|
153
|
+
"""
|
154
|
+
Get Spotify URL from the user
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
str: Entered Spotify URL
|
158
|
+
"""
|
159
|
+
while True:
|
160
|
+
url = Prompt.ask("[purple]Enter Spotify URL[/purple][green]").strip()
|
161
|
+
|
162
|
+
if not url:
|
163
|
+
self.console.print("[red]URL cannot be empty. Please enter a Spotify track URL.[/red]")
|
164
|
+
continue
|
165
|
+
|
166
|
+
if "/track/" in url or "/playlist/" in url:
|
167
|
+
return url
|
168
|
+
|
169
|
+
self.console.print("[red]Invalid format. Please enter a valid Spotify track URL.[/red]")
|
170
|
+
|
171
|
+
def start_message(self):
|
172
|
+
"""Display a stylized start message in the console."""
|
173
|
+
|
174
|
+
msg = r'''
|
175
|
+
_____ _ _ ___
|
176
|
+
| _ |___ ___ ___ _ _ _ ___ ___ _ _ ___ ___ ___| |_|_| _|_ _
|
177
|
+
| | _| _| . | | | | .'| _| |_'_| |_ -| . | . | _| | _| | |
|
178
|
+
|__|__|_| |_| |___|_____|__,|_| |_,_| |___| _|___|_| |_|_| |_ |
|
179
|
+
|_| |___|
|
180
|
+
'''
|
181
|
+
|
182
|
+
if CLEAN_CONSOLE:
|
183
|
+
os.system("cls" if platform.system() == 'Windows' else "clear")
|
184
|
+
|
185
|
+
if SHOW_MESSAGE:
|
186
|
+
self.console.print(f"[purple]{msg}")
|
187
|
+
separator = "_" * (self.console.width - 2)
|
188
|
+
self.console.print(f"[cyan]{separator}[/cyan]\n")
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# 05.04.2024
|
2
|
+
|
3
|
+
import re
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
|
8
|
+
# External imports
|
9
|
+
from unidecode import unidecode
|
10
|
+
|
11
|
+
|
12
|
+
class FileUtils:
|
13
|
+
|
14
|
+
@staticmethod
|
15
|
+
def get_music_folder() -> Path:
|
16
|
+
"""
|
17
|
+
Gets the path to the Music folder.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Path: Path to the Music folder
|
21
|
+
"""
|
22
|
+
music_folder = Path.home() / "Music"
|
23
|
+
if not music_folder.exists():
|
24
|
+
|
25
|
+
# If "Music" does not exist, check for Italian "Musica" and rename it to "Music"
|
26
|
+
musica_folder = Path.home() / "Musica"
|
27
|
+
if musica_folder.exists():
|
28
|
+
musica_folder.rename(music_folder)
|
29
|
+
else:
|
30
|
+
music_folder.mkdir(exist_ok=True)
|
31
|
+
|
32
|
+
return music_folder
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
def sanitize_filename(filename: str) -> str:
|
36
|
+
"""
|
37
|
+
Cleans the filename of invalid characters and applies transliteration.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
filename (str): Filename to clean
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
str: Cleaned filename
|
44
|
+
"""
|
45
|
+
# Transliterate to ASCII
|
46
|
+
filename = unidecode(filename)
|
47
|
+
|
48
|
+
# Remove/replace invalid characters for filenames
|
49
|
+
invalid_chars = '<>:"/\\|?*'
|
50
|
+
for char in invalid_chars:
|
51
|
+
filename = filename.replace(char, '')
|
52
|
+
|
53
|
+
# Remove multiple spaces and trim
|
54
|
+
filename = re.sub(r'\s+', ' ', filename).strip()
|
55
|
+
|
56
|
+
if len(filename) > 200:
|
57
|
+
filename = filename[:200]
|
58
|
+
|
59
|
+
return filename
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def create_filename(artist: str, title: str, extension: str = "mp3") -> str:
|
63
|
+
"""
|
64
|
+
Creates a filename in the format: Artist - Title.extension
|
65
|
+
|
66
|
+
Args:
|
67
|
+
artist (str): Artist name
|
68
|
+
title (str): Song title
|
69
|
+
extension (str): File extension
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
str: Formatted filename
|
73
|
+
"""
|
74
|
+
clean_artist = FileUtils.sanitize_filename(artist)
|
75
|
+
clean_title = FileUtils.sanitize_filename(title)
|
76
|
+
|
77
|
+
# Create format: Artist - Title
|
78
|
+
filename = f"{clean_artist} - {clean_title}"
|
79
|
+
|
80
|
+
return filename
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def get_download_path(artist: str, title: str) -> Path:
|
84
|
+
"""
|
85
|
+
Gets the full path for the download.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
artist (str): Artist name
|
89
|
+
title (str): Song title
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Path: Full file path
|
93
|
+
"""
|
94
|
+
music_folder = FileUtils.get_music_folder()
|
95
|
+
filename = FileUtils.create_filename(artist, title)
|
96
|
+
|
97
|
+
return music_folder / filename
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def file_exists(filepath: Path) -> bool:
|
101
|
+
"""
|
102
|
+
Checks if a file exists.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
filepath (Path): File path
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
bool: True if the file exists
|
109
|
+
"""
|
110
|
+
return filepath.exists()
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def find_downloaded_file(base_path: Path, pattern: str) -> Optional[Path]:
|
114
|
+
"""
|
115
|
+
Finds a downloaded file using a pattern.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
base_path (Path): Base folder for the search
|
119
|
+
pattern (str): Search pattern
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Path: First file found or None
|
123
|
+
"""
|
124
|
+
try:
|
125
|
+
files = list(base_path.glob(pattern))
|
126
|
+
return files[0] if files else None
|
127
|
+
|
128
|
+
except Exception:
|
129
|
+
return None
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# 05.04.2024
|
2
|
+
|
3
|
+
|
4
|
+
# External library
|
5
|
+
import ua_generator
|
6
|
+
|
7
|
+
|
8
|
+
# Variable
|
9
|
+
ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge'))
|
10
|
+
|
11
|
+
|
12
|
+
def get_userAgent() -> str:
|
13
|
+
return ua_generator.generate(device='desktop', browser=('chrome', 'edge')).text
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
def get_headers() -> dict:
|
18
|
+
return ua.headers.get()
|