presenter.py
1 ''' 2 QuickSave allows you to download videos from YotuTube 3 Copyright (C) 2025 Andrés Chaparro 4 5 This program is free software: you can redistribute it and/or modify 6 it under the terms of the GNU General Public License as published by 7 the Free Software Foundation, either version 3 of the License, or 8 (at your option) any later version. 9 10 This program is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 GNU General Public License for more details. 14 15 You should have received a copy of the GNU General Public License 16 along with this program. If not, see <https://www.gnu.org/licenses/>. 17 ''' 18 19 # Importing necesary utilities from kivy 20 from kivy.lang import Builder 21 from kivymd.app import MDApp 22 from kivy.clock import Clock 23 24 from kivy.uix.widget import Widget 25 from kivy.core.window import Window 26 27 import os 28 29 # Importing the MDDropMenu 30 from kivymd.uix.menu import MDDropdownMenu 31 32 # Importing the MDButton 33 from kivymd.uix.button import ( 34 MDButton, 35 MDButtonText, 36 MDButtonIcon 37 ) 38 39 # Importing the MDList 40 from kivymd.uix.list import ( 41 MDListItem, 42 MDListItemHeadlineText, 43 MDListItemLeadingIcon, 44 MDListItemSupportingText 45 ) 46 47 # Importing the MDDialog 48 from kivymd.uix.dialog import ( 49 MDDialog, 50 MDDialogIcon, 51 MDDialogHeadlineText, 52 MDDialogSupportingText, 53 MDDialogButtonContainer, 54 MDDialogContentContainer, 55 ) 56 57 # Importing the MDTextField 58 from kivymd.uix.textfield import ( 59 MDTextField, 60 MDTextFieldLeadingIcon, 61 MDTextFieldHintText, 62 MDTextFieldHelperText, 63 MDTextFieldTrailingIcon, 64 MDTextFieldMaxLengthText, 65 ) 66 67 # Importing custom components 68 from modules.components.drawer_label import DrawerLabel 69 from modules.components.drawer_item import DrawerItem 70 71 # Importing the utilities 72 ''' 73 Allows you to open the browser 74 ''' 75 from modules.utils.open_from_browser import OpenBrowser 76 ''' 77 Display a message in the interface 78 ''' 79 from modules.utils.show_snackbar import ShowSnackbar 80 ''' 81 Manage the MDFileManager for easy use 82 ''' 83 from modules.utils.file_manager_tool import FileManagerTool 84 ''' 85 Allows you to download videos from YouTube 86 ''' 87 from modules.utils.youtube_tool import YoutubeTool 88 89 # Importing persistence 90 from modules.persistence.config_json import ConfigJson 91 92 # Importing messages 93 from screens.message_on_start import MessageOnStart 94 95 #----------- 96 97 # Verify the platform 98 from kivy import platform 99 100 ''' 101 Requesting the necesary permissions on Android. 102 It is also important to set these permissions in 103 the buildozer.spec. 104 ''' 105 if platform == 'android': 106 from android.permissions import request_permissions, Permission 107 108 request_permissions([ 109 Permission.INTERNET, 110 Permission.READ_EXTERNAL_STORAGE, 111 Permission.WRITE_EXTERNAL_STORAGE, 112 Permission.ACCESS_FINE_LOCATION 113 ]) 114 115 # --------- 116 117 # Presenter class, main activity of kivy 118 class Presenter(MDApp): 119 # Global attributes 120 band = True 121 data = object 122 account = object 123 password = object 124 message_on_start = True 125 126 def __init__(self, **kwargs): 127 super().__init__(**kwargs) 128 ''' 129 Handles keyboard or screen events, if not 130 handled there may be exceptions when 131 interacting with the app 132 ''' 133 Window.bind(on_keyboard=self.events) 134 135 ''' 136 Message on startup, only 137 displayed the first time 138 ''' 139 self.message_on_start = MessageOnStart() 140 141 ''' 142 The FileManagerTool will allows you 143 to choose the save path for your videos 144 ''' 145 self.file_manager_tool = FileManagerTool() 146 147 # Persistence 148 self.configurations = ConfigJson() 149 150 # Account in the json 151 self.account = 'user' 152 153 ''' 154 Displaying the startup message if it has not 155 been displayed before 156 ''' 157 if self.configurations.load_topic(self.account, 'message_on_start'): 158 Clock.schedule_once(self.message_on_start.start, 0) 159 160 # Change the startup message from true to false 161 self.configurations.update_data(self.account, 'message_on_start', False) 162 163 # Initiating persistence 164 Clock.schedule_once(self.persistence_data, 0) 165 166 def build(self): 167 168 # Loading each of the screens 169 Builder.load_file("screens/drawer_item.kv") 170 Builder.load_file("screens/drawer_label.kv") 171 Builder.load_file("screens/settings_screen.kv") 172 Builder.load_file("screens/first_screen.kv") 173 return Builder.load_file("screens/main.kv") 174 175 def show_file_manager(self): 176 # Show file manager 177 Clock.schedule_once(self.file_manager_tool.open_file_manager, 0) 178 179 def persistence_data(self, dt): 180 # Putting the first elements on the screen 181 self.start_field() 182 183 # Create the default user 184 for name in self.configurations.load_users(): 185 self.account = name 186 187 if not (self.configurations.store.exists(self.account)): 188 self.configurations.save_user() 189 self.account = 'user' 190 self.password = '1234' 191 192 # Load the theme 193 self.theme_cls.theme_style = self.configurations.load_topic(self.account, 'theme') 194 195 # Load the color scheme 196 self.theme_cls.primary_palette = self.configurations.load_topic(self.account, 'color_scheme') 197 198 def start_field(self): 199 self.text_field_url = MDTextField( 200 MDTextFieldLeadingIcon( 201 icon="link", 202 ), 203 MDTextFieldHintText( 204 text="URL: ", 205 ), 206 mode="outlined", 207 size_hint_x=None, 208 width="240dp", 209 pos_hint={"center_x": 0.5, "center_y": 0.5}, 210 ) 211 212 self.root.get_screen('first').ids.box.add_widget( 213 self.text_field_url 214 ) 215 216 # Button for get data 217 button_get_data = MDButton( 218 MDButtonIcon( 219 icon="chevron-down-circle-outline", 220 ), 221 MDButtonText( 222 text="Obtener", 223 ), 224 225 style="elevated", 226 pos_hint={"center_x": 0.5, "center_y": 0.5}, 227 ) 228 229 button_get_data.bind( 230 on_release=self.get_metadata 231 ) 232 self.root.get_screen('first').ids.box.add_widget( 233 button_get_data 234 ) 235 236 237 def get_metadata(self, *args): 238 ''' 239 This method is responsible for 240 obtaining the video data, 241 sorting it, and displaying it on 242 the screen 243 ''' 244 self.youtube_tool = YoutubeTool(self.text_field_url.text) 245 self.root.get_screen('first').ids.box.clear_widgets() 246 self.start_field() 247 248 # Get metadata 249 metadata = self.youtube_tool.get_metadata() 250 try: 251 ''' 252 Put the id text field and a 253 button to download the video, 254 this is at the top 255 ''' 256 self.text_field_id = MDTextField( 257 MDTextFieldLeadingIcon( 258 icon="identifier", 259 ), 260 MDTextFieldHintText( 261 text="ID: ", 262 ), 263 mode="outlined", 264 size_hint_x=None, 265 width="240dp", 266 pos_hint={"center_x": 0.5, "center_y": 0.5}, 267 ) 268 269 self.root.get_screen('first').ids.box.add_widget( 270 self.text_field_id 271 ) 272 273 button_download = MDButton( 274 MDButtonIcon( 275 icon="download", 276 ), 277 MDButtonText( 278 text="Descargar", 279 ), 280 281 style="elevated", 282 pos_hint={"center_x": 0.5, "center_y": 0.5}, 283 ) 284 button_download.bind( 285 on_release=self.download_video 286 ) 287 self.root.get_screen('first').ids.box.add_widget( 288 button_download 289 ) 290 ''' 291 Organize the highest quality data 292 to the lowest quality data 293 ''' 294 metadata['formats'].reverse() 295 296 ''' 297 Puts each of the video 298 data organized into items 299 ''' 300 for fmt in metadata['formats']: 301 size_mb = f"{int(fmt['filesize']) / (1024 * 1024):.2f} MB" if isinstance(fmt['filesize'], int) else fmt['filesize'] 302 self.root.get_screen('first').ids.box.add_widget( 303 MDListItem( 304 MDListItemLeadingIcon( 305 icon="multimedia", 306 ), 307 MDListItemHeadlineText( 308 text=f"{metadata['title']}", 309 ), 310 MDListItemSupportingText( 311 text=f"ID: {fmt['format_id']} | {fmt['ext']} | {fmt['resolution']} | {size_mb}", 312 ), 313 ) 314 ) 315 316 except Exception as e: 317 self.root.get_screen('first').ids.box.clear_widgets() 318 self.start_field() 319 ShowSnackbar.show(f"No se pudo obtener los datos") 320 print(f"Error in get_metadata: {e}") 321 322 def download_video(self, *args): 323 ''' 324 This method is responsible 325 for downloading the videos 326 ''' 327 self.youtube_tool.download( 328 self.text_field_id.text, 329 self.configurations.load_topic(self.account, 'path') 330 ) 331 332 def open_browser(self, url): 333 """ 334 Method to open 335 the browser 336 """ 337 OpenBrowser.open(url) 338 339 def switch_theme_style(self): 340 ''' 341 This is responsible 342 for changing the subject 343 ''' 344 self.theme_cls.theme_style = ( 345 "Dark" if self.theme_cls.theme_style == "Light" else "Light" 346 ) 347 348 # Displays a message on the screen 349 ShowSnackbar.show(f"Tema actualizado a {self.theme_cls.theme_style}") 350 351 # Save the theme in the json 352 self.configurations.update_data(self.account, 'theme', self.theme_cls.theme_style) 353 354 355 def menu_open(self): 356 ''' 357 A menu to choose color 358 schemes 359 ''' 360 color_scheme = ['Aqua', 'Gold', 'Lime', 'Red', 'Orange', 'Magenta', 'Chocolate', 'Beige'] 361 362 menu_items = [ 363 { 364 "text": color_scheme[i], 365 "on_release": lambda x=color_scheme[i]: self.menu_callback(x), 366 } for i in range(len(color_scheme)) 367 ] 368 369 MDDropdownMenu( 370 position="auto", 371 caller=self.root.get_screen('settings').ids.colors_button, items=menu_items 372 ).open() 373 374 def menu_callback(self, color_scheme): 375 # Update the color scheme 376 ShowSnackbar.show(f"Esquema de color actualizado a {color_scheme}") 377 self.theme_cls.primary_palette = color_scheme 378 379 self.configurations.update_data(self.account, 'color_scheme', color_scheme) 380 381 def on_keyboard(self, window, key, scancode, codepoint, modifier): 382 # Logic for handling keys 383 if key == 27: # key ESC 384 # logic ... 385 return True # Indicates that the event was handled correctly 386 return False # The event was not handled 387 388 def events(self, instance, keyboard, keycode, text, modifiers): 389 '''Called when buttons are pressed on the mobile device.''' 390 try: 391 if keyboard in (1001, 27): 392 if self.manager_open: 393 self.file_manager.back() 394 return True 395 except Exception as e: 396 print(str(f"Error {e}")) 397 return True 398 399 400 401 402 403 404 405 406