from os import makedirs, path from json import load from shutil import copy from types import SimpleNamespace import flet as ft storage = SimpleNamespace() def report(message_:str="", type_:str="error", report_:bool=False): print(f"[{type_}] {message_}") return report_ def getConfig() -> SimpleNamespace: _configJson = { "ui": { "title": "Flet Checker UI", "theme": "dark", "maximized": True, "fullscreen": False, }, "app": { "name": "Flet Checker UI", "version": "0.0.1", "author": "Flet", "description": "Flet checker UI", "license": "MIT", "repository": "" } } _config = SimpleNamespace(**_configJson) for _key in _configJson: setattr(_config, _key, SimpleNamespace(**_configJson[_key])) return _config def getVAllignment(alignment:str="center"): match alignment.lower(): case "center": return ft.MainAxisAlignment.CENTER case "top" | "start": return ft.MainAxisAlignment.START case "bottom" | "end": return ft.MainAxisAlignment.END case _: return ft.MainAxisAlignment.NONE def initApp(page_:ft.Page=None, config_:SimpleNamespace=None) -> bool: if not page_: report("error", "page_ is None", True) if not config_: report("error", "config_ is None", True) page_.title = config_.ui.title page_.theme_mode = config_.ui.theme page_.window_maximized = config_.ui.maximized page_.window_full_screen = config_.ui.fullscreen if hasattr(config_.ui, "vertical_alignment"): page_.vertical_alignment = getVAllignment(config_.ui.vertical_alignment) return True # def channelClickedOld(info:str=None): # return lambda i,j=info: fillChannelVideos(i, j) #print(f"Clicked ", j, i) def channelClicked(event_:ft.ContainerTapEvent=None): #print(event_.control, event_.data, event_.name, event_.page, event_.target) if hasattr(storage, 'selectedChannel'): storage.selectedChannel.content.title.color = ft.colors.WHITE storage.selectedChannel.content.subtitle.color = ft.colors.GREY_400 storage.selectedChannel.bgcolor = ft.colors.TRANSPARENT storage.selectedChannel.update() storage.selectedChannel = event_.control storage.selectedChannel.content.title.color = ft.colors.BLACK storage.selectedChannel.content.subtitle.color = ft.colors.BLACK45 storage.selectedChannel.bgcolor = ft.colors.GREEN_300 storage.selectedChannel.update() fillChannelVideos(event_) def channelUpdate(event_:ft.ControlEvent=None): storage.statusText.value = f"Status: {event_.control.data} updated, not implemented, yet." storage.statusText.update() def channelDisable(event_:ft.ControlEvent=None): storage.statusText.value = f"Status: {event_.control.data} disabled, not implemented, yet." storage.statusText.update() def initUI(page_:ft.Page=None) -> bool: if not page_: report("error", "page_ is None", True) try: storage.lvChannels = ft.ListView( expand=False, spacing=3, padding=5, auto_scroll=False, ) # for i in range(0, 5): # lv.controls.append( # ft.ListTile( # title=ft.Text(f"Line {i}"), # subtitle=ft.Text(f"Videos: 36\nFrequency: Weekly\nChecked: 2024-01-01\nUpdated: 2024-01-01",font_family="consolas", size=10), # on_click=channelClicked(f"Line {i}"), # ), # # ft.Container( # # content=ft.Text( # # f"Line {i}", # # color=ft.colors.WHITE, # # ), # # alignment=ft.alignment.center, # # on_click=channelClicked(f"Line {i}"), # # ), # ) lbChannels = ft.Column( controls=[ ft.Text( "Channels", text_align=ft.TextAlign.CENTER, color=ft.colors.WHITE, style=ft.TextThemeStyle.TITLE_MEDIUM, width=250, ), ft.Divider(height=-1, color=ft.colors.WHITE), ft.Container(content=storage.lvChannels, expand=True) ], spacing=3, ) gvVideos = ft.GridView( expand=True, auto_scroll=False, #horizontal=True, #runs_count=5, max_extent=450, #child_aspect_ratio=1.0, spacing=5, #run_spacing=5, ) storage.statusText = ft.Text("Status: OK", color=ft.colors.WHITE, style=ft.TextThemeStyle.BODY_SMALL) page_.add( ft.SafeArea( content=ft.Column( controls=[ ft.Row( controls=[ ft.Container( content=ft.Text("Action Bar - not implemented, yet."), expand=True, ), ], ), ft.Row( controls=[ ft.Container( content=lbChannels, #ft.Text("Container 1"), border=ft.border.all(color=ft.colors.WHITE, width=1), border_radius=ft.border_radius.all(5), width=250 ), ft.Container( expand=True, content=gvVideos, #content=ft.Text("Container 2"), #bgcolor=ft.colors.RED_100, ), ], expand=True, vertical_alignment=ft.CrossAxisAlignment.STRETCH, ), ft.Row( controls=[ ft.Container( content=storage.statusText, expand=True, ), ], ), ], expand=True, horizontal_alignment=ft.CrossAxisAlignment.STRETCH, ), expand=True, ) ) storage.elements = { "lbChannels": lbChannels, "gvVideos": gvVideos, } return True except Exception as _e: report("error", _e, True) return False def getChannels(file_:str=None) -> bool: if not file_: report("error", f"{file_} is None (getChannels)") if not path.exists(file_): report(f"{file_} does not exist (getChannels)") if not path.isfile(file_): report(f"{file_} is not a file (getChannels)") if not path.splitext(file_)[1] == ".json": report(f"{file_} is not a json file (getChannels)") try: with open(file_) as _jf: _jd = load(_jf) if _jd is None: raise IOError('Error (getChannels): Config file not found!') storage.channels = {} if 'channels' not in _jd else _jd['channels'] if not hasattr(storage, 'channels'): return report('No channels configured (getChannels)') except Exception as _e: print('Exception (getChannels):', _e) return report('Exception (getChannels)!!!') return True def fillChannels(channels_:dict=None) -> bool: if not channels_: return report('No channels configured (fillChannels)') if not hasattr(storage, 'lvChannels'): return report('No lvChannels configured (fillChannels)') if storage.lvChannels is None: return report('lvChannels is None (fillChannels)') try: _sorted = dict(reversed(sorted(channels_.items(), key=lambda x: x[1]['updated']))) for _channel, _values in _sorted.items(): _subtitle = f"Videos: {_values['count']}\n" _subtitle += f"Frequency: {_values['frequency']}\n" _subtitle += f"Checked: {_values['checked']}\n" _subtitle += f"Updated: {_values['updated']}" storage.lvChannels.controls.append( ft.Container( content=ft.ListTile( title=ft.Text(_channel), data=_channel, col=_channel, subtitle=ft.Text(_subtitle,style=ft.TextThemeStyle.BODY_SMALL,color=ft.colors.GREY_400), trailing=ft.PopupMenuButton( icon=ft.icons.MORE_VERT, items=[ ft.PopupMenuItem(text="Update",on_click=channelUpdate,data=_channel), ft.PopupMenuItem(text="Disable",on_click=channelDisable,data=_channel), ], ), #on_click=channelClicked #channelClickedOld(_channel), ), alignment=ft.alignment.top_center, on_click=channelClicked, col=_channel, ), ) storage.lvChannels.update() except Exception as _e: print('Exception (fillChannels):', _e) return report('Exception (fillChannels)!!!') return True def getChannelVideos(file_:str=None) -> bool: if not file_: report("error", f"{file_} is None (getChannelVideos)") if not path.exists(file_): report(f"{file_} does not exist (getChannelVideos)") if not path.isfile(file_): report(f"{file_} is not a file (getChannelVideos)") if not path.splitext(file_)[1] == ".json": report(f"{file_} is not a json file (getChannelVideos)") try: with open(file_) as _jf: _jd = load(_jf) if _jd is None: raise IOError('Error (getChannelVideos): Video file not found!') storage.videos = _jd if not hasattr(storage, 'videos'): return report('No videos configured (getChannelVideos)') except Exception as _e: print('Exception (getChannelVideos):', _e) return report(f'Exception (getChannelVideos) with file "{file_}"!!!') return True def getImage(channel_:str=None, video_id:str=None): if not channel_: return report('No channel configured (getImage)') if not video_id: return report('No video_id configured (getImage)') _file = f'/Users/homeboy/Sources/py/youtube/channels/{channel_}/images/' if not path.exists(_file): makedirs(_file) if not path.exists(_file): return report(f"{_file} does not exist (getImage)") _file += f'{video_id}.jpg' if path.exists(_file): return _file _url = f'https://i.ytimg.com/vi/{video_id}/mqdefault.jpg' try: from urllib.request import urlretrieve urlretrieve(_url, _file) return _file except Exception as _e: print('Exception (getImage):', _e) report('Exception (getImage)!!!') if path.exists('default.jpg'): copy('default.jpg', _file) _file = path.abspath(f'default.jpg') return _file def fillChannelVideos(event_) -> bool: if not event_: return report('No event configured (fillChannelVideos)') #if not hasattr(event_, 'channel'): return report('No channel configured (fillChannelVideos)') #_channel = event_.channel _channel = event_.control.col if not _channel: return report('No channel configured (fillChannelVideos)') if not getChannelVideos(f'/Users/homeboy/Sources/py/youtube/channels/{_channel}/videos.json'): report("fillChannelVideos() failed") _gvVideos = storage.elements['gvVideos'] if not _gvVideos: return report('No gvVideos configured (fillChannelVideos)') _gvVideos.controls = [] storage.statusText.value = f"Status: {_channel} video infos are loading..." storage.statusText.update() _sorted = dict(reversed(sorted(storage.videos.items(), key=lambda x: x[1]['added'] if x else None))) for _videoId, _videoData in _sorted.items(): #print(_videoData['title']) if _videoData['added'] in ['Ignored', 'Deleted']: continue _src = getImage(_channel, _videoId) _title = _videoData['title'] if len(_videoData['title']) < 30 else f"{_videoData['title'][0:30]}..." _length = _videoData["length"] _length = f'{_length}s' if _length > 0 else str(_length) _gvVideos.controls.append( ft.Card( content=ft.Container( content=ft.Column( [ ft.Image( src=_src, width=320, height=200, fit=ft.ImageFit.FILL, repeat=ft.ImageRepeat.NO_REPEAT, border_radius=ft.border_radius.all(5), tooltip=_videoData['title'], ), ft.ListTile( title=ft.Text(_title), subtitle=ft.Text( f"Published: {_videoData['date'][0:10]}\nAdded: {_videoData['added']}\nLength: {_length}\nState: {_videoData['state']}" ), ), ft.Row( [ ft.TextButton("Delete",disabled=True), ft.TextButton("Download",disabled=True), ft.TextButton("Visit",url=f"https://www.youtube.com/watch?v={_videoId}",url_target="_blank") ], alignment=ft.MainAxisAlignment.END, ), ], horizontal_alignment=ft.CrossAxisAlignment.STRETCH, ), width=400, padding=10, alignment=ft.alignment.top_center, ) ) ) _gvVideos.scroll_to(0, 500) _gvVideos.update() storage.statusText.value = f"Status: {_channel} video infos loaded - Ok" storage.statusText.update() return True def main(page: ft.Page) -> None: _config = getConfig() if not _config: return report("_config is None") if not initApp(page, _config): return report("initApp() failed") if not initUI(page): return report("initUI() failed") if not getChannels('/Users/homeboy/Sources/py/youtube/config.json'): return report("getChannels() failed") if not fillChannels(storage.channels): return report("fillChannels() failed") if __name__ == "__main__": exit(ft.app(target=main))