Last active
January 13, 2024 18:25
-
-
Save alexrjs/8ba281290ccef33fea919f21519cf1e5 to your computer and use it in GitHub Desktop.
PoC Youtube Channel Checker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for ui app actionbar | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
# imports | |
from flet import Container, Row, Text, UserControl | |
from flet import colors | |
# classes | |
class ActionBar(UserControl): | |
"""Action bar""" | |
def __init__(self) -> None: | |
super().__init__() | |
def build(self) -> Row: | |
"""Fill action bar""" | |
return Row( | |
controls=[ | |
Container( | |
content=Text("Action Bar - not implemented, yet.", color=colors.RED), | |
expand=True, | |
), | |
], | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for ui app | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
# imports | |
from copy import deepcopy | |
from types import SimpleNamespace | |
from flet import Column, Page, Row, UserControl | |
from flet import CrossAxisAlignment | |
from helpers.db.storage import Storage | |
from helpers.info import report | |
from helpers.ui.bars.action import ActionBar | |
from helpers.ui.bars.status import StatusBar | |
from helpers.ui.boxes.channels import ListBoxChannels | |
from helpers.ui.boxes.videos import GridViewVideos | |
# classes | |
class FletApp(UserControl): | |
"""Flet app""" | |
def __init__(self, page_:Page=None, config_:dict=None) -> None: | |
"""Init""" | |
if not page_: raise AttributeError("Page is None (FletApp)") | |
if not config_: raise AttributeError("Config is None (FletApp)") | |
if type(config_) is not SimpleNamespace: raise AttributeError("Config is not a SimpleNamespace (FletApp)") | |
super().__init__() | |
self.page = page_ | |
self.config = deepcopy(config_) | |
self.elements = Storage() | |
self.update() | |
def fillChannels(self, channels_:dict=None) -> None: | |
"""Fill channels listbox""" | |
if not channels_: raise AttributeError("Channels is None (FletApp)") | |
if type(channels_) is not dict: raise AttributeError("Channels is not a list (FletApp)") | |
self.elements.channelContainer.channelsList.controls = [] | |
if not self.elements.channelContainer.fillChannels(channels_): report("Channels not filled (FletApp.fillChannels)") | |
self.elements.statusbar.setStatus("Channels loaded.") | |
return True | |
def build(self) -> Column: | |
"""Build app ui""" | |
self.elements.actionbar = ActionBar() | |
self.elements.channelContainer = ListBoxChannels() | |
self.elements.videosContainer = GridViewVideos() | |
self.elements.content = Row( | |
controls=[ | |
self.elements.channelContainer, | |
self.elements.videosContainer, | |
], | |
vertical_alignment=CrossAxisAlignment.STRETCH, | |
expand=True, | |
) | |
self.elements.statusbar = StatusBar() | |
return Column( | |
controls=[ | |
# Row: Action bar | |
self.elements.actionbar, | |
# Row: Main content | |
self.elements.content, | |
# Row: Status bar | |
self.elements.statusbar | |
], | |
horizontal_alignment=CrossAxisAlignment.STRETCH, | |
expand=True, | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for ui listbox channels | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
from flet import Column, Container, ContainerTapEvent, ControlEvent, Divider, ListTile, ListView, PopupMenuButton, PopupMenuItem, Text, UserControl | |
from flet import TextAlign, TextThemeStyle | |
from flet import alignment, border, border_radius, colors, icons | |
from helpers.info import report | |
class ListEntryChannel(UserControl): | |
"""Channel listbox entry""" | |
def __init__(self, parent, **kwargs): | |
super().__init__(parent, **kwargs) | |
self._id = kwargs['id'] if 'id' in kwargs else None | |
self._name = kwargs['name'] if 'name' in kwargs else None | |
self._selected = kwargs['selected'] if 'selected' in kwargs else None | |
self._onSelect = kwargs['onSelect'] if 'onSelect' in kwargs else None | |
self._label = self.addLabel() | |
self._label.text = self._name | |
self._label.onSelect = self._onSelect | |
if self._selected: | |
self._label.select() | |
def _onSelect(self, sender, args): | |
"""On select channel""" | |
self._selected = True | |
def _onDeselect(self, sender, args): | |
"""On deselect channel""" | |
self._selected = False | |
class ListBoxChannels(UserControl): | |
"""Channels listbox""" | |
def __init__(self): | |
super().__init__() | |
self.channelsList = None | |
self.channelsBox = None | |
self.selectedChannel = None | |
def channelClicked(self, event_:ContainerTapEvent=None): | |
#print(event_.control, event_.data, event_.name, event_.page, event_.target) | |
if self.selectedChannel: | |
self.selectedChannel.content.title.color = colors.WHITE | |
self.selectedChannel.content.subtitle.color = colors.GREY_400 | |
self.selectedChannel.bgcolor = colors.TRANSPARENT | |
self.selectedChannel.update() | |
self.selectedChannel = event_.control | |
self.selectedChannel.content.title.color = colors.BLACK | |
self.selectedChannel.content.subtitle.color = colors.BLACK45 | |
self.selectedChannel.bgcolor = colors.GREEN_300 | |
self.selectedChannel.update() | |
#fillChannelVideos(event_) | |
def channelUpdate(self, event_:ControlEvent=None): | |
print(f"Status: {event_.control.data} updated, not implemented, yet.") | |
def channelDisable(self, event_:ControlEvent=None): | |
print(f"Status: {event_.control.data} disabled, not implemented, yet.") | |
def fillChannels(self, channels_:dict=None) -> bool: | |
if not channels_: return report('No channels configured (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']}" | |
self.channelsList.controls.append( | |
Container( | |
content=ListTile( | |
title=Text(_channel), | |
data=_channel, | |
col=_channel, | |
subtitle=Text(_subtitle,style=TextThemeStyle.BODY_SMALL,color=colors.GREY_400), | |
trailing=PopupMenuButton( | |
icon=icons.MORE_VERT, | |
items=[ | |
PopupMenuItem(text="Update",on_click=self.channelUpdate,data=_channel), | |
PopupMenuItem(text="Disable",on_click=self.channelDisable,data=_channel), | |
], | |
), | |
), | |
alignment=alignment.top_center, | |
on_click=self.channelClicked, | |
col=_channel, | |
), | |
) | |
self.channelsList.update() | |
except Exception as _e: | |
print('Exception (fillChannels):', _e) | |
return report('Exception (fillChannels)!!!') | |
return True | |
def build(self): | |
self.channelsList = ListView( | |
expand=False, | |
spacing=3, | |
padding=5, | |
auto_scroll=False, | |
) | |
self.channelsBox = Column( | |
controls=[ | |
Text( | |
"Channels", | |
text_align=TextAlign.CENTER, | |
color=colors.WHITE, | |
style=TextThemeStyle.TITLE_MEDIUM, | |
width=250, | |
), | |
Divider(height=-1, color=colors.WHITE), | |
self.channelsList | |
], | |
spacing=3, | |
) | |
return Container( | |
content=self.channelsBox, | |
border=border.all(color=colors.WHITE, width=1), | |
border_radius=border_radius.all(5), | |
width=250, | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Simple UI for youtube checker | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
# imports | |
from os import path | |
from flet import app, Page, MainAxisAlignment | |
from helpers.info import report | |
from helpers.db.assets import getVideoThumbnail | |
from helpers.db.config import getConfig | |
from helpers.db.storage import Storage | |
from helpers.db.video import getChannelVideos, getChannels | |
from helpers.ui.app import FletApp | |
from helpers.ui.config import getVAllignment | |
# variables | |
storage = Storage() | |
storage.assetBase = path.dirname(path.abspath(__file__)) | |
storage.fileBase = '/Users/homeboy/Sources/py/youtube' | |
storage.channelsConfig = f'{storage.fileBase}/config.json' | |
storage.channelVideosBase = f'{storage.fileBase}/channels' | |
# 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'{storage.channelVideosBase}/{_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 = getVideoThumbnail(_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_: Page) -> None: | |
if not page_: report("error", "page_ is None!", True) | |
_config = getConfig() | |
if not _config: return report("_config is None!") | |
page_.title = 'FletApp' if not hasattr(_config.ui, 'title') else _config.ui.title | |
page_.theme_mode = 'system' if not hasattr(_config.ui, 'theme') else _config.ui.theme | |
page_.window_maximized = False if not hasattr(_config.ui, 'maximized') else _config.ui.maximized | |
page_.window_full_screen = False if not hasattr(_config.ui, 'fullscreen') else _config.ui.fullscreen | |
page_.vertical_alignment = MainAxisAlignment.NONE if not hasattr(_config.ui, "vertical_alignment") else getVAllignment(_config.ui.vertical_alignment) | |
storage.app = FletApp(page_, _config) | |
if not storage.app: return report("No app created!") | |
page_.add(storage.app) | |
storage.app.elements.statusbar.setStatus("Ready.") | |
# get and fill channels | |
storage.channels = getChannels(storage.channelsConfig) | |
if not storage.channels: return report("getChannels() failed") | |
if not storage.app.fillChannels(storage.channels): return report("fillChannels() failed") | |
if __name__ == "__main__": | |
exit(app(target=main)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"channels": { | |
"AlexiBexi": { | |
"checked": "2024-01-08", | |
"count": 36, | |
"frequency": "weekly", | |
"id": "UCzH549YlZhdhIqhtvz7XHmQ", | |
"name": "AlexiBexi", | |
"updated": "2024-01-08" | |
}, | |
"Apfeltalk": { | |
"checked": "2024-01-08", | |
"count": 52, | |
"frequency": "weekly", | |
"id": "UC7XqBsdIv5Yyjrq5JXLzwpw", | |
"name": "Apfeltalk", | |
"updated": "2024-01-08" | |
}, | |
"Apfelwelt": { | |
"checked": "2024-01-12", | |
"count": 88, | |
"frequency": "bidaily", | |
"id": "UCVChR9pqkB2Y6jpY8MlesoA", | |
"name": "Apfelwelt", | |
"updated": "2024-01-10" | |
}, | |
"AwesomeOpenSource": { | |
"checked": "2024-01-02", | |
"count": 39, | |
"frequency": "monthly", | |
"id": "UCwFpzG5MK5Shg_ncAhrgr9g", | |
"name": "AwesomeOpenSource", | |
"updated": "2024-01-02" | |
}, | |
"RafaelZeier": { | |
"checked": "2024-01-13", | |
"count": 222, | |
"frequency": "daily", | |
"id": "UCMW8XBBRkjrF_fweALWaZyw", | |
"name": "RafaelZeier", | |
"updated": "2024-01-11" | |
}, | |
"mpoxDE": { | |
"checked": "2024-01-13", | |
"count": 40, | |
"frequency": "daily", | |
"id": "UCJZnRJATAFN5rbXpewp7Vyw", | |
"name": "mpoxDE", | |
"updated": "2024-01-11" | |
} | |
}, | |
"updated": "2024-01-13" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for config database | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
from types import SimpleNamespace | |
from helpers.info import report | |
def getConfig() -> SimpleNamespace: | |
"""Get config object""" | |
#TODO: Read config from file | |
_configJson = { | |
"ui": { | |
"title": "Flet Checker UI", | |
"theme": "dark", | |
"maximized": True, | |
"fullscreen": False, | |
}, | |
"infos": { | |
"statusBar": "Ready." | |
}, | |
"app": { | |
"name": "Flet Checker UI", | |
"version": "0.0.1", | |
"author": "Flet", | |
"description": "Flet checker UI", | |
"license": "MIT", | |
"repository": "" | |
} | |
} | |
_config = SimpleNamespace(**_configJson) | |
try: | |
for _key in _configJson: | |
setattr(_config, _key, SimpleNamespace(**_configJson[_key])) | |
return _config | |
except Exception as e: | |
report(f"Error converting key '{_key}' value: {e}") | |
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for video database | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
from os import path | |
from json import load | |
from helpers.info import report | |
from helpers.db.storage import Storage | |
def getChannels(file_:str=None) -> dict: | |
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!') | |
if 'channels' not in _jd: return report('No channels configured (getChannels)') | |
return {} if 'channels' not in _jd else _jd['channels'] | |
except Exception as _e: | |
print('Exception (getChannels):', _e) | |
return report('Exception (getChannels)!!!') | |
return True | |
def getChannelVideos(file_:str=None) -> dict: | |
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!') | |
return _jd | |
except Exception as _e: | |
print('Exception (getChannelVideos):', _e) | |
report(f'Exception (getChannelVideos) with file "{file_}"!!!') | |
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for reporting stuff | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
def report(message_:str="", type_:str="error", report_:bool=False) -> bool: | |
"""Prints a message to the console and returns a boolean value.""" | |
print(f"[{type_}] {message_}") | |
return report_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for ui app statusbar | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
# imports | |
from flet import Container, Row, Text, UserControl | |
from flet import colors, TextThemeStyle | |
# classes | |
class StatusBar(UserControl): | |
"""Status bar""" | |
def __init__(self) -> None: | |
"""Init""" | |
super().__init__() | |
def setStatus(self, status:str='') -> None: | |
"""Set status""" | |
self.status.value = status | |
self.update() | |
def build(self) -> Row: | |
"""Fill action bar""" | |
self.status = Text('', color=colors.WHITE, style=TextThemeStyle.BODY_SMALL) | |
return Row( | |
controls=[ | |
Container( | |
content=self.status, | |
expand=True, | |
), | |
], | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Data storage class for config | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
import json | |
from types import SimpleNamespace | |
class Storage(SimpleNamespace): | |
"""Storage class for config""" | |
def has(self, key): | |
return hasattr(self, key) | |
def dumpToJson(self, file_path): | |
"""Dump the content to a JSON file for debug purposes""" | |
try: | |
with open(file_path, 'w') as f: | |
json.dump(self.__dict__, f, indent=4) | |
except Exception as e: | |
raise IOError(f"An error occurred while dumping to JSON: {e}") | |
def __enter__(self): | |
if isinstance(self.__dict__, dict): | |
return self.__dict__.__enter__() | |
else: | |
raise AttributeError("'Storage' object has no attribute '__enter__'") | |
def __exit__(self, exc_type, exc_value, traceback): | |
if isinstance(self.__dict__, dict): | |
return self.__dict__.__exit__(exc_type, exc_value, traceback) | |
else: | |
raise AttributeError("'Storage' object has no attribute '__exit__'") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
. | |
├── checkeruipoc.py | |
├── assets | |
│ └── default.jpg | |
├── checkerui.py | |
├── config.json | |
├── helpers | |
│ ├── __init__.py | |
│ ├── db | |
│ │ ├── __init__.py | |
│ │ ├── assets.py | |
│ │ ├── config.py | |
│ │ ├── storage.py | |
│ │ └── video.py | |
│ ├── info.py | |
│ └── ui | |
│ ├── __init__.py | |
│ ├── app.py | |
│ ├── bars | |
│ │ ├── __init__.py | |
│ │ ├── action.py | |
│ │ └── status.py | |
│ ├── boxes | |
│ │ ├── __init__.py | |
│ │ ├── channels.py | |
│ │ └── videos.py | |
│ └── config.py | |
├── requirements.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
- Helper module for ui listbox channels | |
- Author: alexrjs | |
- License: Unlicense | |
""" | |
from flet import Container, GridView, UserControl | |
from flet import border, border_radius, colors | |
class GridEntryVideo(UserControl): | |
"""Video grid entry""" | |
def __init__(self, parent, **kwargs): | |
super().__init__(parent, **kwargs) | |
self._id = kwargs['id'] if 'id' in kwargs else None | |
self._name = kwargs['name'] if 'name' in kwargs else None | |
self._selected = kwargs['selected'] if 'selected' in kwargs else None | |
self._onSelect = kwargs['onSelect'] if 'onSelect' in kwargs else None | |
self._label = self.addLabel() | |
self._label.text = self._name | |
self._label.onSelect = self._onSelect | |
if self._selected: | |
self._label.select() | |
def _onSelect(self, sender, args): | |
"""On select channel""" | |
self._selected = True | |
def _onDeselect(self, sender, args): | |
"""On deselect channel""" | |
self._selected = False | |
class GridViewVideos(UserControl): | |
"""Channels listbox""" | |
def __init__(self): | |
super().__init__() | |
self.videosGrid = None | |
def build(self): | |
self.videosGrid = GridView( | |
expand=True, | |
auto_scroll=False, | |
max_extent=450, | |
spacing=5, | |
) | |
return Container( | |
content=self.videosGrid, | |
border=border.all(color=colors.WHITE, width=1), | |
border_radius=border_radius.all(5), | |
expand=True, | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment