1+ from dataclasses import dataclass
12import argparse
2- from os import name , system
3- import questionary
4- import shutil
5- import sys
63import logging
74import platform
5+ import shutil
6+ import sys
7+ from os import name , system
8+ from pathlib import Path
9+
10+ import questionary
811
912from .chapters import get_chapters
1013from .search import search_book
1114
1215# Terminal settings for Unix-like systems
1316if platform .system () != "Windows" :
1417 import termios
18+
1519 fd : int = sys .stdin .fileno ()
1620 old_term_settings : list [int ] = termios .tcgetattr (fd )
1721else :
1822 termios = None
1923
24+ from .utils import setup_colored_logging
25+
2026# Configure logging
21- logging .basicConfig (
22- level = logging .INFO ,
23- format = "%(asctime)s - %(levelname)s - %(message)s" ,
24- datefmt = "%Y-%m-%d %H:%M:%S" ,
25- )
27+ logger = logging .getLogger (__name__ )
28+
29+
30+ @dataclass
31+ class DownloadConfig :
32+ directory : Path | None
33+ verbose : bool
34+ show_all_chapter_bars : bool
2635
2736
2837def check_ffmpeg () -> None :
2938 """Check if ffmpeg is available in PATH."""
3039 if not shutil .which ("ffmpeg" ):
31- print ( "Error: ffmpeg is required but not found in PATH" )
32- print ("Please install ffmpeg:" )
33- print (" macOS: brew install ffmpeg" )
34- print (" Ubuntu/Debian: sudo apt install ffmpeg" )
35- print (" Windows: Download from https://ffmpeg.org/download.html" )
40+ logger . error ( " ffmpeg is required but not found in PATH" )
41+ logger . info ("Please install ffmpeg:" )
42+ logger . info (" macOS: brew install ffmpeg" )
43+ logger . info (" Ubuntu/Debian: sudo apt install ffmpeg" )
44+ logger . info (" Windows: Download from https://ffmpeg.org/download.html" )
3645 sys .exit (1 )
3746
3847
@@ -41,77 +50,154 @@ def clear_terminal() -> None:
4150 system ("cls" if name == "nt" else "clear" ) # noqa: S605
4251
4352
44- def get_input ():
45- """Get URL input from user for manual download."""
46- url = input ("Enter URL: " )
47-
53+ def validate_url (url : str ) -> str :
54+ """Validate and return a validated url."""
4855 if not url .startswith ("https://tokybook.com/" ):
49- print ("Invalid URL!" )
50- return get_input ()
51-
56+ raise ValueError (f"Invalid URL: { url } . Must start with https://tokybook.com/" )
5257 return url
5358
5459
55- def main () -> None :
56- """Get argument from CLI and execute the selected action."""
57- # Check for ffmpeg first
58- check_ffmpeg ()
59-
60+ def parse_arguments () -> argparse .Namespace :
61+ """Parse command line arguments."""
6062 parser = argparse .ArgumentParser (
6163 description = "TokySnatcher - Download Audiobooks from TokyBook"
6264 )
6365 parser .add_argument (
6466 "-d" , "--directory" , type = str , default = None , help = "Custom download directory"
6567 )
6668 parser .add_argument (
67- "-s" , "--search" , type = str , default = None , help = "Search query to bypass interactive menu"
69+ "-s" ,
70+ "--search" ,
71+ type = str ,
72+ default = None ,
73+ help = "Search query to bypass interactive menu" ,
6874 )
6975 parser .add_argument (
70- "-u" , "--url" , type = str , default = None , help = "Direct URL to download, bypassing search"
76+ "-u" ,
77+ "--url" ,
78+ type = str ,
79+ default = None ,
80+ help = "Direct URL to download, bypassing search" ,
7181 )
72- args = parser .parse_args ()
82+ parser .add_argument (
83+ "-v" ,
84+ "--verbose" ,
85+ action = "store_true" ,
86+ help = "Show detailed logs during download" ,
87+ )
88+ parser .add_argument (
89+ "-a" ,
90+ "--show-all-chapter-bars" ,
91+ action = "store_true" ,
92+ default = False ,
93+ help = "Show all chapter progress bars permanently" ,
94+ )
95+ return parser .parse_args ()
7396
97+
98+ def execute_download (url : str , config : DownloadConfig ) -> None :
99+ """Execute the download process for a given URL."""
100+ from .utils import _shutdown_requested
101+
102+ logger .info ("Download starting." )
103+ get_chapters (
104+ url ,
105+ config .directory ,
106+ verbose = config .verbose ,
107+ show_all_chapter_bars = config .show_all_chapter_bars ,
108+ )
109+
110+ # Check if download was interrupted
111+ if _shutdown_requested :
112+ logger .warning ("Download was cancelled by user." )
113+
114+
115+ def handle_url_action (url : str , config : DownloadConfig ) -> None :
116+ """Handle direct URL download."""
117+ validated_url = validate_url (url )
118+ logger .info ("Downloading from provided URL." )
119+ execute_download (validated_url , config )
120+
121+
122+ def handle_search_action (query : str , config : DownloadConfig ) -> None :
123+ """Handle search action."""
124+ result = search_book (query , interactive = True )
125+ if result :
126+ execute_download (result , config )
127+
128+
129+ def handle_interactive_action (config : DownloadConfig ) -> None :
130+ """Handle interactive menu selection."""
131+ choices = ["Search book" , "Download from URL" , "Exit" ]
132+ selected_action = questionary .select ("Choose action:" , choices = choices ).ask ()
133+
134+ if selected_action is None :
135+ logger .info ("No action selected. Exiting." )
136+ return
137+
138+ actions = {
139+ "Search book" : lambda : handle_search_action (
140+ input ("Enter search query: " ), config
141+ ),
142+ "Download from URL" : lambda : handle_url_action (get_validated_input (), config ),
143+ "Exit" : lambda : None ,
144+ }
145+
146+ action = actions .get (selected_action )
147+ if action :
148+ action ()
149+
150+
151+ def get_validated_input () -> str :
152+ """Get validated URL input from user."""
153+ url = input ("Enter URL: " )
74154 try :
75- # If URL provided, skip everything and download directly
76- if args .url :
77- logging .info ("Downloading from provided URL." )
78- get_chapters (args .url , args .directory )
79- return
80-
81- # If search query provided, skip interactive menu
82- if args .search :
83- result = search_book (args .search )
84- if result :
85- logging .info ("Action completed successfully." )
86- get_chapters (result , args .directory )
87- return
88-
89- # Define available choices
90- choices = ["Search book" , "Download from URL" , "Exit" ]
91- selected_action = questionary .select ("Choose action:" , choices = choices ).ask ()
92-
93- if selected_action is None :
94- logging .info ("No action selected. Exiting." )
95- sys .exit (0 )
96-
97- # Map actions to corresponding functions
98- actions = [search_book , get_input , sys .exit ]
99- action = actions [choices .index (selected_action )]
100- result = action ()
101-
102- if result :
103- logging .info ("Action completed successfully." )
104- get_chapters (result , args .directory )
155+ return validate_url (url )
156+ except ValueError as e :
157+ print (f"Error: { e } " )
158+ return get_validated_input ()
159+
160+
161+ def get_input () -> str :
162+ """Get URL input from user for manual download. Deprecated, use get_validated_input."""
163+ while True :
164+ url = input ("Enter URL: " )
165+ try :
166+ return validate_url (url )
167+ except ValueError as e :
168+ print (f"Error: { e } " )
169+ continue
170+
105171
172+ def main () -> None :
173+ """Get argument from CLI and execute the selected action."""
174+ check_ffmpeg ()
175+ args = parse_arguments ()
176+ setup_colored_logging (args .verbose )
177+
178+ config = DownloadConfig (
179+ directory = Path (args .directory ) if args .directory else None ,
180+ verbose = args .verbose ,
181+ show_all_chapter_bars = args .show_all_chapter_bars ,
182+ )
183+
184+ try :
185+ if args .url :
186+ handle_url_action (args .url , config )
187+ elif args .search :
188+ handle_search_action (args .search , config )
189+ else :
190+ handle_interactive_action (config )
106191 except KeyboardInterrupt :
107- logging .warning ("Process interrupted by user." )
192+ logger .warning ("Process interrupted by user." )
108193 sys .exit (0 )
109- except Exception as e :
110- logging . error ( f "An unexpected error occurred: { e } " )
194+ except Exception :
195+ logger . exception ( "An unexpected error occurred" )
111196 sys .exit (1 )
112197 finally :
113198 if termios :
114199 termios .tcsetattr (fd , termios .TCSADRAIN , old_term_settings )
115200
201+
116202if __name__ == "__main__" :
117203 main ()
0 commit comments