class ProgramWindow(ctk.CTkToplevel):
"""
Window application to display the program configuration.
"""
DAYS = {'Lu': "Lunedì", 'Ma': "Martedì", 'Me': "Mercoledì", 'Gi': "Giovedì",
'Ve': "Venerdì", 'Sa': "Sabato", 'Do': "Domenica"}
TYPES = {'recurring': 'ricorrente', 'onetime': 'one-time'}
def __init__(self, parent: MainWindow, categories: dict[str, str],
playlists: dict[str, str]) -> NoReturn:
super().__init__(parent)
self.title("Programmazione")
self.geometry("750x490")
self.resizable(False, False)
self.iconbitmap(MainWindow.ICON)
self.__font = ('Arial', 14)
self.__hover = '#21cbf0'
self.categories = categories
self.playlists = {'': '-- NESSUNA --'}
self.playlists.update(playlists)
self.__set_day = False
self.__set_time = False
self.withdraw()
self.__gui = self.__make_layout(categories, self.playlists)
self.iconbitmap(MainWindow.ICON)
self.focus_force()
self.deiconify()
self.attributes('-topmost', 1)
self.after(200, lambda: self.iconbitmap(MainWindow.ICON))
self.after(1000, lambda: self.attributes('-topmost', 0))
for t in range(100, 1000, 50):
self.after(t, self.focus_force)
def __make_layout(self, categories: dict[str, str],
playlists: dict[str, str]) -> dict[str, Any]:
"""
Create the layout of the program window.
:param categories: Dictionary of YouTube categories.
:param playlists: Dictionary of channel playlists.
:return: The layout of the program window.
"""
gui = {}
self.columnconfigure(2, weight=1)
l1 = ctk.CTkLabel(self, text='Titolo:', font=self.__font)
l1.grid(row=0, column=0, sticky='w', padx=(20, 10), pady=(15, 5))
l2 = ctk.CTkLabel(self, text='Descrizione:', font=self.__font)
l2.grid(row=1, column=0, sticky='w', padx=(20, 10))
gui['title'] = ctk.StringVar()
e1 = ctk.CTkEntry(self, font=self.__font, textvariable=gui['title'],
border_width=1)
e1.grid(row=0, column=1, sticky="ew", pady=(15, 5))
gui['description'] = ctk.CTkTextbox(self, font=self.__font, height=100,
width=400, border_width=1,
wrap='word')
gui['description'].grid(row=1, column=1, sticky="ns")
l3 = ctk.CTkLabel(self, text="Variabili utilizzabili: $DAY$ $TIME$",
font=('Arial', 12, 'italic'))
l3.grid(row=2, column=1, sticky='w')
self.__make_switch_frame(gui)
self.__make_central_frame(categories, gui, playlists)
f1 = ctk.CTkFrame(self, height=0, border_width=2, border_color='gray85')
f1.grid(row=4, column=0, columnspan=3, pady=5, ipady=8)
icon = TablerIcons.load(FilledIcon.ARROW_BIG_LEFT_LINES, size=100)
gui['first_button'] = ctk.CTkButton(f1, text='', width=40, height=40,
image=CTkImage(icon))
gui['first_button'].pack(expand=False, side='left', padx=3)
icon = TablerIcons.load(FilledIcon.ARROW_BIG_LEFT, size=100)
gui['prev_button'] = ctk.CTkButton(f1, text='', width=40, height=40,
image=CTkImage(icon))
gui['prev_button'].pack(expand=False, side='left')
gui['progress'] = ctk.CTkLabel(f1, text="0 di 0", width=80, height=40)
gui['progress'].pack(expand=False, side='left')
icon = TablerIcons.load(FilledIcon.ARROW_BIG_RIGHT, size=100)
gui['next_button'] = ctk.CTkButton(f1, text='', width=40, height=40,
image=CTkImage(icon))
gui['next_button'].pack(expand=False, side='left')
icon = TablerIcons.load(FilledIcon.ARROW_BIG_RIGHT_LINES, size=100)
gui['last_button'] = ctk.CTkButton(f1, text='', width=40, height=40,
image=CTkImage(icon))
gui['last_button'].pack(expand=False, side='left', padx=3)
self.__make_actions_frame(gui)
return gui
def __make_switch_frame(self, gui: dict[str, Any]) -> None:
"""
Create the switch frame.
:param gui: The layout of the program window.
"""
frame = ctk.CTkFrame(self, height=0)
frame.grid(row=0, column=2, rowspan=3, padx=20, pady=(15, 0),
sticky='nsew')
switches = {'enabled': 'Abilitato',
'enableDvr': 'Attiva DVR',
'autoStart': 'Avvio automatico',
'autoStop': 'Arresto automatico',
'enable_presets': 'Abilita preset'}
for i, (k, v) in enumerate(switches.items()):
gui[k] = ctk.BooleanVar()
switch = ctk.CTkSwitch(frame, text=v, variable=gui[k])
switch.grid(row=i, column=0, padx=15, pady=3, sticky='nsew')
frame.rowconfigure(i, weight=1)
def __make_central_frame(self, categories: dict[str, str],
gui: dict[str, Any],
playlists: dict[str, str]) -> None:
"""
Create the central frame of the program window.
:param categories: Dictionary of YouTube categories.
:param gui: The layout of the program window.
:param playlists: Dictionary of channel playlists.
"""
frame = ctk.CTkFrame(self, height=0)
frame.grid(row=3, column=0, columnspan=3, padx=20, pady=10, sticky='ew')
frame.columnconfigure(tuple(range(4)), weight=1)
l1 = ctk.CTkLabel(frame, text="Categoria video")
l1.grid(row=0, column=0, padx=15, pady=(10, 2), sticky="w")
gui['category'] = ctk.StringVar()
cb1 = ctk.CTkOptionMenu(frame, variable=gui['category'])
cb1.grid(row=1, column=0, padx=10, pady=(0, 15), sticky='ew')
CTKDropdown(cb1, justify="left", width=170, height=160,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=sorted(list(categories.values())))
l2 = ctk.CTkLabel(frame, text="Privacy")
l2.grid(row=0, column=1, padx=15, pady=(10, 2), sticky="w")
gui['privacy'] = ctk.StringVar()
cb2 = ctk.CTkOptionMenu(frame, variable=gui['privacy'], width=100)
cb2.grid(row=1, column=1, padx=10, pady=(0, 15), sticky='ew')
CTKDropdown(cb2, width=140, height=120, scrollbar=False,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=['public', 'unlisted', 'private'])
l3 = ctk.CTkLabel(frame, text="Playlist")
l3.grid(row=0, column=2, padx=15, pady=(10, 2), sticky="w")
gui['playlist'] = ctk.StringVar()
cb3 = ctk.CTkOptionMenu(frame, variable=gui['playlist'])
cb3.grid(row=1, column=2, padx=10, pady=(0, 15), sticky='ew')
CTKDropdown(cb3, justify="left", width=300, height=160,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=sorted(list(playlists.values())))
l4 = ctk.CTkLabel(frame, text="Tipologia live")
l4.grid(row=0, column=3, padx=15, pady=(10, 2), sticky="w")
gui['type'] = ctk.StringVar(value='ricorrente')
cb4 = ctk.CTkOptionMenu(frame, variable=gui['type'], width=100)
cb4.grid(row=1, column=3, padx=10, pady=(0, 15), sticky='ew')
CTKDropdown(cb4, width=140, height=85, scrollbar=False,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=self.TYPES.values())
gui['type'].set('one-time')
self.__make_timing_frame(frame, gui)
self.after(0, lambda: gui['type'].set('ricorrente'))
def __make_timing_frame(self, parent: ctk.CTkFrame,
gui: dict[str, Any]) -> None:
"""
Create the timing frame.
:param parent: The parent frame.
:param gui: The layout of the program window.
"""
frame = ctk.CTkFrame(parent, height=0, fg_color='transparent')
frame.grid(row=2, column=0, columnspan=4, padx=0, pady=(0, 15),
sticky='ew')
frame.columnconfigure(tuple(range(6)), weight=1)
frame.columnconfigure((0, 2), weight=0)
l1 = ctk.CTkLabel(frame, text="Preparazione")
l1.grid(row=0, column=0, columnspan=2, padx=15, pady=0, sticky="w")
gui['prepare'] = CTkSpinbox(frame, start=60, border_width=1, width=110)
gui['prepare'].grid(row=1, column=0, padx=(10, 5), pady=0, sticky='e')
gui['prepare_unit'] = ctk.StringVar(value='minuti')
cb1 = ctk.CTkOptionMenu(frame, variable=gui['prepare_unit'], width=85)
cb1.grid(row=1, column=1, padx=(0, 10), pady=0, sticky='ew')
CTKDropdown(cb1, width=140, height=118, scrollbar=False,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=['minuti', 'ore', 'giorni'])
l2 = ctk.CTkLabel(frame, text="Durata live")
l2.grid(row=0, column=2, columnspan=2, padx=15, pady=0, sticky="w")
gui['duration'] = CTkSpinbox(frame, start=60, border_width=1, width=110)
gui['duration'].grid(row=1, column=2, padx=(10, 5), pady=0, sticky='e')
gui['duration_unit'] = ctk.StringVar(value='minuti')
cb2 = ctk.CTkOptionMenu(frame, variable=gui['duration_unit'], width=85)
cb2.grid(row=1, column=3, padx=(0, 10), pady=0, sticky='ew')
CTKDropdown(cb2, width=140, height=118, scrollbar=False,
button_height=30, frame_corner_radius=7,
hover_color=self.__hover, frame_border_width=1,
values=['minuti', 'ore', 'giorni'])
l3 = ctk.CTkLabel(frame, text="Data della live")
l3.grid(row=0, column=4, padx=15, pady=0, sticky="w")
gui['day'] = ctk.StringVar()
var1 = ctk.StringVar()
cb3 = ctk.CTkOptionMenu(frame, variable=var1, width=100)
CTKDropdown(cb3, width=140, height=150, button_height=30,
frame_corner_radius=7, hover_color=self.__hover,
frame_border_width=1, values=list(self.DAYS.values()))
e1 = ctk.CTkEntry(frame, font=self.__font, border_width=1, width=100,
placeholder_text='gg/mm/aaaa')
e1.bind('<KeyRelease>', lambda e: self.__day_change(cb3, e1, event=e))
var1.trace_add('write', lambda a, b, c: self.__day_change(cb3, e1))
gui['type'].trace_add('write', lambda *a: self.__manage_day(cb3, e1))
l4 = ctk.CTkLabel(frame, text="Orario")
l4.grid(row=0, column=5, padx=15, pady=0, sticky="w")
gui['time'] = ctk.StringVar()
cb4 = ctk.CTkComboBox(frame, variable=gui['time'], width=100)
gui['time'].trace_add('write', lambda *a: self.__valid_time(*a, cb=cb4))
tdb = [f'{h:02d}:{m:02d}' for h in range(24) for m in range(0, 60, 30)]
CTKDropdown(cb4, width=140, height=150, button_height=30,
frame_corner_radius=7, hover_color=self.__hover,
frame_border_width=1, values=tdb)
cb4.grid(row=1, column=5, padx=10, pady=0, sticky='ew')
def __manage_day(self, day: ctk.CTkOptionMenu, date: ctk.CTkEntry) -> None:
"""
Show or hide the day and date widgets in the timing frame
depending on the type.
:param day: The day widget.
:param date: The date widget.
"""
if self.__gui['type'].get() == 'one-time':
date.grid(row=1, column=4, padx=10, pady=0, sticky='ew')
if date.get() == '':
# pylint: disable=protected-access
# noinspection PyProtectedMember
self.after(5, date._entry_focus_out)
day.grid_remove()
else:
day.grid(row=1, column=4, padx=10, pady=0, sticky='ew')
date.grid_remove()
if self.__set_day:
date.delete(0, 'end')
if self.__gui['type'].get() == 'ricorrente':
day.set(self.__gui['day'].get())
else:
day.set('Domenica')
self.__set_day = False
self.__day_change(day, date)
def __day_change(self, day: ctk.CTkOptionMenu, date: ctk.CTkEntry,
event: Event = None) -> bool:
"""
Callback for the day changed event.
:param day: The day widget.
:param date: The date widget.
"""
if self.__gui['type'].get() == 'ricorrente':
self.__gui['day'].set(day.get())
return True
keys = ('BackSpace', 'Delete', 'Left', 'Right', 'Up', 'Down', 'Home',
'End', 'Escape', 'Page_Up', 'Page_Down', 'Shift_L', 'Shift_R',
'Control_L', 'Control_R')
if event is not None and event.keysym in keys:
return True
value = self.__gui['day'].get() if self.__set_day else date.get()
value = ''.join(c for c in value if c.isdigit() or c == '/')
parts = value.split('/')
match (len(parts)):
case 1:
match (len(parts[0])):
case 0:
value = ''
case 1:
value = parts[0]
case 2:
value = f'{min(31, int(parts[0])):02d}/'
case _:
dd = f'{min(31, int(parts[0][:2])):02d}/'
mm = f'{min(12, int(parts[0][2:]))}'
value = f'{dd}{mm}'
case 2:
mm = ''
yy = ''
match (len(parts[0])):
case 0:
dd = '01'
case 1 | 2:
dd = f'{min(31, int(parts[0])):02d}'
case _:
dd = f'{min(31, int(parts[0][:2])):02d}'
mm = f'{min(12, int(parts[0][2:]))}'
match (len(parts[1])):
case 0:
pass
case 1:
mm = parts[1]
case 2:
mm = f'{min(12, int(parts[1])):02d}'
if dd == '31' and int(mm) in (4, 6, 9, 11):
dd = '30'
if dd in ('30', '31') and int(mm) == 2:
dd = '29'
mm += '/'
case _:
mm = f'{min(12, int(parts[1][:2])):02d}'
yy = f'{min(9999, int(parts[1][2:]))}'
if dd == '31' and int(mm) in (4, 6, 9, 11):
dd = '30'
if dd in ('30', '31') and int(mm) == 2:
dd = '29'
mm += '/'
value = f'{dd}/{mm}{yy}'
case _:
mm = '01'
yy = ''
match (len(parts[0])):
case 0:
dd = '01'
case 1 | 2:
dd = f'{min(31, int(parts[0])):02d}'
case _:
dd = f'{min(31, int(parts[0][:2])):02d}'
mm = f'{min(12, int(parts[0][2:]))}'
match (len(parts[1])):
case 0:
pass
case 1 | 2:
mm = f'{min(12, int(parts[1])):02d}'
case _:
mm = f'{min(12, int(parts[1][:2])):02d}'
yy = f'{min(9999, int(parts[1][2:]))}'
match (len(parts[2])):
case 0:
pass
case 1 | 2 | 3:
yy = f'{min(9999, int(parts[2]))}'
case _:
yy = f'{min(9999, int(parts[2][:4]))}'
if dd == '31' and int(mm) in (4, 6, 9, 11):
dd = '30'
if dd in ('30', '31') and int(mm) == 2:
dd = '29'
if yy != '':
y = int(yy)
bis = y % 4 == 0 and y % 100 != 0 or y % 400 == 0
if len(yy) == 4 and dd == '29' and int(mm) == 2 and not bis:
dd = '28'
value = f'{dd}/{mm}/{yy}'
date.delete(0, 'end')
date.insert(0, value)
self.after(5, lambda: date.icursor(len(value)))
self.__gui['day'].set(value)
return True
# pylint: disable=unused-argument
# noinspection PyUnusedLocal
def __valid_time(self, *args, cb: ctk.CTkComboBox) -> None:
"""
Callback for the time changed event.
"""
value = self.__gui['time'].get()
value = ''.join(c for c in value if c.isdigit() or c == ':')
parts = value.split(':')
match (len(parts)):
case 1:
match (len(parts[0])):
case 0:
value = ''
case 1:
value = parts[0]
case 2:
value = f'{min(23, int(parts[0])):02d}:'
# pylint: disable=protected-access
# noinspection PyProtectedMember
self.after(5, lambda: cb._entry.icursor(len(value)))
case _:
hh = f'{min(23, int(parts[0][:2])):02d}:'
mm = f'{min(59, int(parts[0][2:]))}'
value = f'{hh}{mm}'
# pylint: disable=protected-access
# noinspection PyProtectedMember
self.after(5, lambda: cb._entry.icursor(len(value)))
self.__gui['time'].set(value)
case _:
match (len(parts[0])):
case 0:
hh = '00'
# pylint: disable=protected-access
# noinspection PyProtectedMember
self.after(5, lambda: cb._entry.icursor(3))
case 1 | 2:
hh = f'{min(23, int(parts[0])):02d}'
case _:
hh = f'{min(23, int(parts[0][:2])):02d}'
match (len(parts[1])):
case 0 | 1:
mm = parts[1]
case 2:
mm = f'{min(59, int(parts[1])):02d}'
case _:
mm = f'{min(59, int(parts[1][:2])):02d}'
value = f'{hh}:{mm}'
if self.__set_time:
self.after(5, lambda: self.__gui['time'].set(value))
self.__set_time = False
else:
self.__gui['time'].set(value)
def __make_actions_frame(self, gui):
f1 = ctk.CTkFrame(self, height=0, fg_color='transparent')
f1.grid(row=5, column=0, columnspan=3, pady=20)
icon = TablerIcons.load(OutlineIcon.DEVICE_FLOPPY, size=100)
gui['save_button'] = ctk.CTkButton(f1, text='Salva', width=100,
height=35, image=CTkImage(icon))
gui['save_button'].pack(expand=False, side='left')
icon = TablerIcons.load(OutlineIcon.FILE_PLUS, size=100)
gui['new_button'] = ctk.CTkButton(f1, text='Nuovo', width=100,
height=35, image=CTkImage(icon))
gui['new_button'].pack(expand=False, side='left', padx=15)
icon = TablerIcons.load(OutlineIcon.COPY_PLUS, size=100)
gui['copy_button'] = ctk.CTkButton(f1, text='Duplica', width=100,
height=35, image=CTkImage(icon))
gui['copy_button'].pack(expand=False, side='left')
icon = TablerIcons.load(OutlineIcon.TRASH, size=100)
gui['delete_button'] = ctk.CTkButton(f1, text='Cancella', width=100,
height=35, image=CTkImage(icon))
gui['delete_button'].pack(expand=False, side='left', padx=15)
icon = TablerIcons.load(OutlineIcon.RELOAD, size=100)
gui['reload_button'] = ctk.CTkButton(f1, text='Ricarica', width=100,
height=35, image=CTkImage(icon))
gui['reload_button'].pack(expand=False, side='left')
def set_first_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the first button.
:param callback: The callback of the button.
"""
self.__gui['first_button'].configure(command=callback)
def set_prev_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the previous button.
:param callback: The callback of the button.
"""
self.__gui['prev_button'].configure(command=callback)
def set_next_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the next button.
:param callback: The callback of the button.
"""
self.__gui['next_button'].configure(command=callback)
def set_last_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the last button.
:param callback: The callback of the button.
"""
self.__gui['last_button'].configure(command=callback)
def set_save_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the save button.
:param callback: The callback of the button.
"""
self.__gui['save_button'].configure(command=callback)
def set_new_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the new button.
:param callback: The callback of the button.
"""
self.__gui['new_button'].configure(command=callback)
def set_copy_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the copy button.
:param callback: The callback of the button.
"""
self.__gui['copy_button'].configure(command=callback)
def set_delete_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the delete button.
:param callback: The callback of the button.
"""
self.__gui['delete_button'].configure(command=callback)
def set_reload_button_callback(self, callback: Callable) -> None:
"""
Set the callback of the reload button.
:param callback: The callback of the button.
"""
self.__gui['reload_button'].configure(command=callback)
def get(self) -> dict[str, Any]:
"""
Get the program data.
:return: The program data as a dictionary.
"""
program: dict[str, Any] = {
'title': self.__gui['title'].get().strip(),
'description': self.__gui['description'].get('1.0', 'end').strip(),
'enabled': self.__gui['enabled'].get(),
'enableDvr': self.__gui['enableDvr'].get(),
'autoStart': self.__gui['autoStart'].get(),
'autoStop': self.__gui['autoStop'].get(),
'enable_presets': self.__gui['enable_presets'].get(),
'privacy': self.__gui['privacy'].get(),
'prepare': self.__to_minutes(self.__gui['prepare'].get(),
self.__gui['prepare_unit'].get()),
'duration': self.__to_minutes(self.__gui['duration'].get(),
self.__gui['duration_unit'].get()),
'time': self.__gui['time'].get()
}
try:
program['type'] = {k for k, v in self.TYPES.items()
if v == self.__gui['type'].get()}.pop()
except KeyError:
program['type'] = None
try:
category = {k for k, v in self.categories.items()
if v == self.__gui['category'].get()}.pop()
program['category'] = int(category)
except (KeyError, ValueError):
program['category'] = None
try:
playlist = {k for k, v in self.playlists.items()
if v == self.__gui['playlist'].get()}.pop()
program['playlist'] = playlist
except KeyError:
program['playlist'] = None
try:
if self.__gui['type'].get() == 'one-time':
program['day'] = self.__gui['day'].get()
if self.__gui['type'].get() == 'ricorrente':
program['day'] = {k for k, v in self.DAYS.items()
if v == self.__gui['day'].get()}.pop()
except KeyError:
program['day'] = ''
return program
def set(self, program: dict[str, Any] = None, n: int = None,
tot: int = None) -> None:
"""
Set the program data.
:param program: The program data as a dictionary.
:param n: The current program number.
:param tot: The total number of programs.
"""
if program is None:
program = {}
n = tot = None
if n is not None and tot is not None:
self.__gui['progress'].configure(text=f'{n} di {tot}')
color = ThemeManager.theme["CTkButton"]["fg_color"]
kwargs = {'state': 'disabled' if n == 1 else 'normal',
'fg_color': 'gray60' if n == 1 else color}
self.__gui['first_button'].configure(**kwargs)
self.__gui['prev_button'].configure(**kwargs)
kwargs = {'state': 'disabled' if n == tot else 'normal',
'fg_color': 'gray60' if n == tot else color}
self.__gui['next_button'].configure(**kwargs)
self.__gui['last_button'].configure(**kwargs)
else:
self.__gui['progress'].configure(text='new')
kwargs = {'state': 'disabled', 'fg_color': 'gray60'}
self.__gui['first_button'].configure(**kwargs)
self.__gui['prev_button'].configure(**kwargs)
self.__gui['next_button'].configure(**kwargs)
self.__gui['last_button'].configure(**kwargs)
self.__gui['title'].set(str(program.get('title', '')))
self.__gui['description'].delete('1.0', 'end')
description = str(program.get('description', ''))
self.__gui['description'].insert('1.0', description)
for k in ('enabled', 'enableDvr', 'autoStart', 'autoStop',
'enable_presets'):
self.__gui[k].set(bool(program.get(k, False)))
first_category = sorted(list(self.categories.values()))[0]
self.__gui['category'].set(self.categories.get(
str(program.get('category')), first_category))
p_list = ['public', 'unlisted', 'private']
privacy = program.get('privacy')
self.__gui['privacy'].set(privacy if privacy in p_list else 'public')
playlist = str(program.get('playlist', ''))
self.__gui['playlist'].set(self.playlists.get(playlist))
self.__gui['type'].set(self.TYPES.get(program.get('type'), 'one-time'))
time, unit = self.__to_time_unit(program, 'prepare')
self.__gui['prepare'].set(time)
self.__gui['prepare_unit'].set(unit)
time, unit = self.__to_time_unit(program, 'duration')
self.__gui['duration'].set(time)
self.__gui['duration_unit'].set(unit)
if self.__gui['type'].get() == 'ricorrente':
self.__gui['day'].set(self.DAYS.get(program.get('day'), 'Domenica'))
else:
self.__gui['day'].set(program.get('day', ''))
self.__set_day = self.__set_time = True
self.__gui['type'].set(self.__gui['type'].get())
self.__gui['time'].set(str(program.get('time', '')))
@staticmethod
def __to_time_unit(program: dict[str, Any], key: str) -> tuple[int, str]:
"""
Convert the time value to the time unit.
:param program: The program data as a dictionary.
:param key: String key to convert from the dictionary.
:return: A tuple with the time and the unit.
"""
time = int(program.get(key, 60))
unit = 'minuti'
if time % 1440 == 0:
unit = 'giorni'
time //= 1440
elif time % 60 == 0:
unit = 'ore'
time //= 60
return time, unit
@staticmethod
def __to_minutes(time: str, unit: str) -> int | None:
"""
Convert the time to minutes.
:param time: The time value.
:param unit: The time unit.
:return: The time in minutes.
"""
if time == '':
return None
time = int(time)
if unit == 'ore':
time *= 60
elif unit == 'giorni':
time *= 1440
return time