-
Notifications
You must be signed in to change notification settings - Fork 178
Expand file tree
/
Copy pathmain.py
More file actions
399 lines (337 loc) · 14.7 KB
/
Copy pathmain.py
File metadata and controls
399 lines (337 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
from pathlib import Path
from bs4 import BeautifulSoup
import requests
from urllib.parse import unquote
import sys
import json
import os
# colorama init (cross-platform ANSI support)
try:
from colorama import init, Fore, Style
init(autoreset=True)
except Exception:
class _Dummy:
def __getattr__(self, _):
return ''
Fore = _Dummy()
Style = _Dummy()
# Import helpers from split modules
from utils.helper import (
collect_filtered_hrefs,
count_audio_hrefs,
parse_star_selection,
ensure_emoji_spacing,
clear_screen,
print_banner,
clear_below_banner,
format_terminal_link,
supports_terminal_links,
)
from utils.downloader import download_links
# Re-export parse_star_selection for backward compatibility
__all__ = ["parse_star_selection"]
# Banner, author and version
BANNER = r'''
/$$$$$$$$ /$$ /$$ /$$ /$$ /$$$$$$$ /$$$$$$
|__ $$__/ |__/| $$ | $$$ /$$$| $$__ $$ /$$__ $$
| $$ /$$$$$$ /$$$$$$/$$$$ /$$| $$ | $$$$ /$$$$| $$ \ $$|__/ \ $$
| $$ |____ $$| $$_ $$_ $$| $$| $$ | $$ $$/$$ $$| $$$$$$$/ /$$$$$/
| $$ /$$$$$$$| $$ \ $$ \ $$| $$| $$ | $$ $$$| $$| $$____/ |___ $$
| $$ /$$__ $$| $$ | $$ | $$| $$| $$ | $$\ $ | $$| $$ /$$ \ $$
| $$| $$$$$$$| $$ | $$ | $$| $$| $$ | $$ \/ | $$| $$ | $$$$$$/
|__/ \_______/|__/ |__/ |__/|__/|__/ |__/ |__/|__/ \______/
Tamil MP3 Downloader - Author: Anbuselvan Rocky
'''
# Color choices
BANNER_COLOR = Fore.CYAN + Style.BRIGHT
MENU_COLOR = Fore.YELLOW + Style.BRIGHT
PROMPT_COLOR = Fore.GREEN + Style.BRIGHT
SUCCESS_COLOR = Fore.GREEN
ERROR_COLOR = Fore.RED
INFO_COLOR = Fore.MAGENTA
COMING_SOON_COLOR = Fore.MAGENTA + Style.DIM
RESET = Style.RESET_ALL
CATEGORIES = {
'1': 'Star Hits',
'2': 'Music Director Hits',
'3': 'Singer Hits',
'4': 'Old Songs',
'5': 'Ring tones / Instrumentals',
'6': 'By Genre',
}
def goodbye_and_exit():
try:
print('\n' + INFO_COLOR + 'GoodBye Nanba/Nanbis!' + RESET)
except Exception:
print('\nGoodBye Nanba/Nanbis!')
sys.exit(0)
def get_resource_path(rel_path: str) -> str:
"""Return an absolute path to a resource, working both when running from source
and when bundled by PyInstaller (where resources are extracted to sys._MEIPASS).
rel_path should be the path relative to the project root, for example: 'data/star-hits.json'
"""
try:
# If running as a PyInstaller bundle, files are in sys._MEIPASS
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
base = sys._MEIPASS
else:
# Use the directory containing main.py as the project root
base = os.path.dirname(os.path.abspath(__file__))
# Normalize separators and join into a single relative path string
rel_normalized = rel_path.replace('/', os.sep).lstrip(os.sep)
return os.path.join(base, rel_normalized)
except Exception:
return rel_path
def read_version() -> str:
"""Read version string from VERSION file located at the project root (bundled via PyInstaller)."""
try:
vpath = get_resource_path('VERSION')
with open(vpath, 'r', encoding='utf-8') as vf:
ver = vf.read().strip()
if ver:
return ver
except Exception:
pass
return '0.0.0'
def print_banner_and_menu():
# full clear then print banner
clear_screen()
# Replace the author name with a clickable terminal link when supported
author_name = 'Anbuselvan Rocky'
github_url = 'https://github.com/anburocky3'
# Insert the version inline next to the author name inside the banner.
# Read runtime version first (fallback to 0.0.0 on error)
try:
version = read_version()
except Exception:
version = '0.0.0'
try:
if supports_terminal_links():
formatted_author = format_terminal_link(author_name, github_url)
banner_to_print = BANNER.replace(author_name, f"{formatted_author} - v{version}")
else:
banner_to_print = BANNER.replace(author_name, f"{author_name} | v{version}")
except Exception:
# On any error, fall back to the original banner without inline version
banner_to_print = BANNER
print_banner(banner_to_print, BANNER_COLOR, RESET)
print(MENU_COLOR + 'What would you like to download? (type the number; type "exit" to quit)' + RESET)
# only display actual numbered categories
for k, v in CATEGORIES.items():
# Present 'By Genre' (6) as coming soon so users know it's not implemented
if k == '6':
print(f" {Fore.CYAN}{k}{RESET}. {COMING_SOON_COLOR}{v} (Coming Soon){RESET}")
else:
print(f" {Fore.CYAN}{k}{RESET}. {v}")
print(f"")
def prompt_choice(prompt: str, default: str = '') -> str:
return input(PROMPT_COLOR + prompt + RESET).strip() or default
def show_and_download(index_url: str, category_name: str, save_subpath_default: str):
clear_below_banner(BANNER, BANNER_COLOR, RESET)
try:
page = requests.get(index_url, timeout=30)
page.raise_for_status()
except Exception as e:
print(ERROR_COLOR + f"Failed to fetch URL {index_url}: {e}" + RESET)
return
soup = BeautifulSoup(page.content, 'html.parser')
hrefs = collect_filtered_hrefs(soup)
audio_count = count_audio_hrefs(hrefs)
print("")
print(INFO_COLOR + ensure_emoji_spacing(f"👋 I found {audio_count} audio links for {save_subpath_default}.\n") + RESET)
# build output directory
out_dir = Path(f'output/{category_name}/{save_subpath_default}')
print(INFO_COLOR + f"Saving to: {out_dir}" + RESET)
out_dir.mkdir(parents=True, exist_ok=True)
# Download audio links (downloader.download_links provides a default chunk_size)
total_audio, successful, failed = download_links(hrefs, index_url, out_dir)
print(INFO_COLOR + ensure_emoji_spacing('\n\n' + '-----------------------------------------------------------') + RESET)
print(INFO_COLOR + ensure_emoji_spacing(f'💁\u200d♂️ Total Downloadable files: {total_audio}') + RESET)
print(SUCCESS_COLOR + ensure_emoji_spacing(f'✅ Successful: {successful}') + RESET)
print(ERROR_COLOR + ensure_emoji_spacing(f"❌ Couldn't download: {failed}") + RESET)
print(INFO_COLOR + ensure_emoji_spacing('-----------------------------------------------------------') + RESET)
def handle_data_category(data_file: str, category_name: str):
"""Load a JSON list of {id, href/path, name} and drive the same flow as Star Hits.
data_file: path to JSON file under data/
category_name: top-level category used for output path
"""
try:
data_path = get_resource_path(data_file)
with open(data_path, 'r', encoding='utf-8') as f:
data_list = json.load(f)
except Exception as e:
print(ERROR_COLOR + f"Failed to load {data_file}: {e}" + RESET)
return
# Display list (clear below the banner so banner remains visible)
clear_below_banner(BANNER, BANNER_COLOR, RESET)
print(MENU_COLOR + f"Available {category_name}:" + RESET)
for item in data_list:
print(f" {Fore.CYAN}{item.get('id')}{RESET}. {item.get('name')}")
print(f" {Fore.CYAN}all{RESET}. Download ALL entries")
print(f"")
sel = prompt_choice("Select number(s)/ranges (e.g. 1,3,5 or 2-4) or 'all' (back/exit): ")
if not sel:
print(ERROR_COLOR + "No selection made, returning to menu." + RESET)
return
if sel.lower() in ('back', 'b'):
return
if sel.lower() in ('exit', '0', 'quit'):
goodbye_and_exit()
# Special case: when viewing the Ringtones list, pressing '6' should open the
# prepopulated list from data/new-movies-ringtone.json
try:
if data_file.endswith('ringtones.json') and sel.strip() == '6':
# Delegate to the new-movies JSON file (drill-down)
new_file = 'data/new-movies-ringtone.json'
# Use a friendly category name when showing the nested list
handle_data_category(new_file, 'New Movies Ring Tone')
return
except Exception:
# ignore and continue with normal flow on unexpected errors
pass
selected_items, missing = parse_star_selection(sel, data_list)
for mid in missing:
print(ERROR_COLOR + f"No item with id {mid}, skipping." + RESET)
if not selected_items:
print(ERROR_COLOR + f"No valid selections found, returning to menu." + RESET)
return
for item in selected_items:
item_name = item.get('name')
# prefer 'href' then 'path'
index_url = item.get('href') or item.get('path')
if not index_url:
print(ERROR_COLOR + f"No URL for '{item_name}', skipping." + RESET)
continue
user_path = prompt_choice(f"Enter The Path To Save Files inside '{category_name}/{item_name}': (DEFAULT: {item_name}) (skip/back/exit): ", item_name)
if user_path.lower() in ('skip', 's'):
print(INFO_COLOR + f"Skipped {item_name}" + RESET)
continue
if user_path.lower() in ('back', 'b'):
break
if user_path.lower() in ('exit', '0', 'quit'):
goodbye_and_exit()
dir_path = Path(f'output/{category_name}/{user_path}')
print(INFO_COLOR + '----------------------------------------------')
print(INFO_COLOR + f' 🎼 Files will be saved to: {dir_path}')
print(INFO_COLOR + '----------------------------------------------' + RESET)
dir_path.mkdir(parents=True, exist_ok=True)
# Reuse show_and_download which fetches index, filters links and downloads
show_and_download(index_url, category_name, user_path)
def handle_old_songs():
"""Show a small submenu for 'Old Songs' and delegate to handle_data_category.
Options:
1 - Old Collections -> data/old/collections.json
2 - Old Hits (Singers) -> data/old/singers.json
"""
clear_below_banner(BANNER, BANNER_COLOR, RESET)
print(MENU_COLOR + "Old Songs - select a subcategory:" + RESET)
print(f" {Fore.CYAN}1{RESET}. Old Collections")
print(f" {Fore.CYAN}2{RESET}. Old Hits (Singers)")
print(f" {Fore.CYAN}b{RESET}. Back to main menu")
print(f"")
sel = prompt_choice("Enter choice (1-2, b/back): ")
if not sel:
print(ERROR_COLOR + "No selection made, returning to menu." + RESET)
return
sel_low = sel.lower()
if sel_low in ('b', 'back'):
return
if sel_low in ('exit', '0', 'quit'):
goodbye_and_exit()
if sel_low == '1':
handle_data_category('data/old/collections.json', 'Old Collections')
elif sel_low == '2':
handle_data_category('data/old/singers.json', 'Old Hits (Singers)')
else:
print(ERROR_COLOR + f"Invalid choice '{sel}', returning to menu." + RESET)
def handle_ringtones_instrumentals():
"""Submenu for Ringtones and Instrumental Collections.
Options:
1 - Ringtones -> data/ringtones.json
2 - Instrumentals -> data/instrumentals.json
"""
clear_below_banner(BANNER, BANNER_COLOR, RESET)
print(MENU_COLOR + "Ring Tones / Instrumentals - select a subcategory:" + RESET)
print(f" {Fore.CYAN}1{RESET}. Ringtones")
print(f" {Fore.CYAN}2{RESET}. Instrumentals")
print(f" {Fore.CYAN}b{RESET}. Back to main menu")
print(f"")
sel = prompt_choice("Enter choice (1-2, b/back): ")
if not sel:
print(ERROR_COLOR + "No selection made, returning to menu." + RESET)
return
sel_low = sel.lower()
if sel_low in ('b', 'back'):
return
if sel_low in ('exit', '0', 'quit'):
goodbye_and_exit()
if sel_low == '1':
handle_data_category('data/ringtones.json', 'Ring Tones')
elif sel_low == '2':
handle_data_category('data/instrumentals.json', 'Instrumental Collections')
else:
print(ERROR_COLOR + f"Invalid choice '{sel}', returning to menu." + RESET)
def main():
while True:
# Show banner and menu, get a valid choice
print_banner_and_menu()
choice = prompt_choice('Enter choice (1-7) [default 1]: ', '1').lower()
if choice in ('0', 'exit', 'quit'):
goodbye_and_exit()
if choice not in CATEGORIES:
print(ERROR_COLOR + f"Invalid choice '{choice}', please try again." + RESET)
continue
category_name = CATEGORIES[choice]
print(INFO_COLOR + f"Selected: {category_name}\n" + RESET)
# Special flows for Star Hits (1), Music Director Hits (2) and Singer Hits (3)
if choice in ('1', '2', '3'):
mapping = {
'1': 'data/star-hits.json',
'2': 'data/music-directors-hits.json',
'3': 'data/singer-hits.json',
}
data_file = mapping.get(choice)
handle_data_category(data_file, category_name)
continue
# Handle Old Songs subcategories
if choice == '4':
handle_old_songs()
continue
# Handle Ring tones / Instrumentals subcategories
if choice == '5':
handle_ringtones_instrumentals()
continue
# Handle By Genre as 'coming soon'
if choice == '6':
clear_below_banner(BANNER, BANNER_COLOR, RESET)
print(COMING_SOON_COLOR + '🚧 By Genre is coming soon. This feature is not implemented yet.' + RESET)
_ = prompt_choice('\nPress Enter to return to the main menu...')
continue
# Generic flow for other categories
index_url = prompt_choice("Enter 'Index Of' URL (back/exit): ")
if not index_url:
print(ERROR_COLOR + "No URL provided, returning to menu." + RESET)
continue
if index_url.lower() in ('back', 'b'):
continue
if index_url.lower() in ('exit', '0', 'quit'):
goodbye_and_exit()
try:
project_name = unquote(index_url.rsplit('/', 2)[1])
except Exception:
project_name = unquote(index_url.rstrip('/').rsplit('/', 1)[-1])
user_path = prompt_choice(f"Enter The Path To Save Files inside '{category_name}': (DEFAULT: {project_name}) (back/exit): ", project_name)
if user_path.lower() in ('back', 'b'):
continue
if user_path.lower() in ('exit', '0', 'quit'):
goodbye_and_exit()
dir_path = Path(f'output/{category_name}/{user_path}')
print(INFO_COLOR + str(dir_path) + RESET)
dir_path.mkdir(parents=True, exist_ok=True)
# Fetch index and download
show_and_download(index_url, category_name, user_path)
if __name__ == '__main__':
try:
main()
except (KeyboardInterrupt, EOFError):
goodbye_and_exit()