added recordings tab to the control panel

This commit is contained in:
DYefremov
2021-08-06 13:23:06 +03:00
parent 95aa8aaed6
commit 872f3f0f81
4 changed files with 276 additions and 10 deletions

View File

@@ -510,6 +510,7 @@ class HttpAPI:
INFO = "about" INFO = "about"
SIGNAL = "signal" SIGNAL = "signal"
STREAM = "stream.m3u?ref=" STREAM = "stream.m3u?ref="
STREAM_TS = "ts.m3u?file="
STREAM_CURRENT = "streamcurrent.m3u" STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent" CURRENT = "getcurrent"
TEST = None TEST = None
@@ -531,6 +532,10 @@ class HttpAPI:
# Timer # Timer
TIMER = "" TIMER = ""
TIMER_LIST = "timerlist" TIMER_LIST = "timerlist"
# Recordings
RECORDINGS = "movielist?dirname="
REC_DIRS = "getlocations"
REC_CURRENT = "getcurrlocation"
# Screenshot # Screenshot
GRUB = "grab?format=jpg&" GRUB = "grab?format=jpg&"
@@ -557,6 +562,17 @@ class HttpAPI:
WAKEUP = "4" WAKEUP = "4"
STANDBY = "5" STANDBY = "5"
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,
Request.EPG,
Request.TIMER,
Request.RECORDINGS}
STREAM_REQUESTS = {Request.STREAM,
Request.STREAM_CURRENT,
Request.STREAM_TS}
def __init__(self, settings): def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -577,18 +593,14 @@ class HttpAPI:
url = self._base_url + req_type.value url = self._base_url + req_type.value
data = self._data data = self._data
if req_type is self.Request.ZAP or req_type is self.Request.STREAM: if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
url += urllib.parse.quote(ref) url += urllib.parse.quote(ref)
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE: elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A")) url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is self.Request.GRUB: elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security. data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref) url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in (self.Request.REMOTE, elif req_type in self.PARAM_REQUESTS:
self.Request.POWER,
self.Request.VOL,
self.Request.EPG,
self.Request.TIMER):
url += ref url += ref
def done_callback(f): def done_callback(f):
@@ -632,7 +644,7 @@ class HttpAPI:
def get_response(req_type, url, data=None): def get_response(req_type, url, data=None):
try: try:
with urlopen(Request(url, data=data), timeout=10) as f: with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT: if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")} return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB: elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()} return {"img_data": f.read()}
@@ -648,6 +660,11 @@ def get_response(req_type, url, data=None):
elif req_type is HttpAPI.Request.TIMER_LIST: elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]} ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else: else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()} return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e: except HTTPError as e:

View File

@@ -325,6 +325,11 @@ Author: Dmitriy Yefremov
<property name="icon_name">view-refresh</property> <property name="icon_name">view-refresh</property>
<property name="icon_size">1</property> <property name="icon_size">1</property>
</object> </object>
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash</property>
</object>
<object class="GtkImage" id="restart_gui_image"> <object class="GtkImage" id="restart_gui_image">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -1721,6 +1726,103 @@ audio-volume-medium-symbolic</property>
<property name="position">3</property> <property name="position">3</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport" id="recordings_view_port">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="recordings_list_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">multiple</property>
<property name="activate_on_single_click">False</property>
<signal name="button-press-event" handler="on_recordings_press" swapped="no"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="recordings_dir_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="changed" handler="on_recordings_dir_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_action_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkSearchEntry" id="recordings_filter_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">tools-check-spelling</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">Filter</property>
<signal name="search-changed" handler="on_recording_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="recordings_remove_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="action_name">app.on_recording_remove</property>
<property name="image">remove_recording_image</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="name">recordings</property>
<property name="title" translatable="yes">Recordings</property>
<property name="position">4</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View File

@@ -9,7 +9,7 @@ from gi.repository import GLib
from .dialogs import show_dialog, DialogType, get_message, get_builder from .dialogs import show_dialog, DialogType, get_message, get_builder
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI from ..connections import HttpAPI, UtfFTP
from ..eparser.ecommons import BqServiceType from ..eparser.ecommons import BqServiceType
@@ -22,6 +22,7 @@ class ControlBox(Gtk.HBox):
EPG = "epg" EPG = "epg"
TIMERS = "timers" TIMERS = "timers"
TIMER = "timer" TIMER = "timer"
RECORDINGS = "recordings"
class EpgRow(Gtk.ListBoxRow): class EpgRow(Gtk.ListBoxRow):
def __init__(self, event: dict, **properties): def __init__(self, event: dict, **properties):
@@ -111,6 +112,75 @@ class ControlBox(Gtk.HBox):
EVENT = 1 EVENT = 1
CHANGE = 2 CHANGE = 2
class RecordingsRow(Gtk.ListBoxRow):
def __init__(self, movie: dict, **properties):
super().__init__(**properties)
self._movie = movie
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._service = movie.get("e2servicename")
service_label = Gtk.Label()
service_label.set_markup("<b>{}</b>".format(self._service))
self._title = movie.get("e2title", "")
title_label = Gtk.Label(self._title)
self._desc = movie.get("e2description", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start_time = datetime.fromtimestamp(int(movie.get("e2time", "0")))
start_time_label = Gtk.Label()
start_time_label.set_margin_top(5)
start_time_label.set_markup("<b>{}</b>".format(start_time.strftime("%A, %H:%M")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
time_label.set_markup("<b>{}</b>".format(movie.get("e2length", "0")))
info_box = Gtk.HBox()
info_box.set_orientation(Gtk.Orientation.HORIZONTAL)
info_box.set_spacing(10)
info_box.pack_start(start_time_label, False, True, 5)
info_box.pack_end(time_label, False, True, 5)
h_box.add(service_label)
h_box.add(title_label)
h_box.add(description)
h_box.add(info_box)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.set_tooltip_text(movie.get("e2filename", ""))
self.add(h_box)
self.show_all()
@property
def movie(self):
return self._movie
@property
def service(self):
return self._service or ""
@property
def title(self):
return self._title or ""
@property
def desc(self):
return self._desc or ""
@property
def file(self):
return self._movie.get("e2filename", "")
def __init__(self, app, http_api, settings, *args, **kwargs): def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -127,7 +197,10 @@ class ControlBox(Gtk.HBox):
"on_epg_press": self.on_epg_press, "on_epg_press": self.on_epg_press,
"on_epg_filter_changed": self.on_epg_filter_changed, "on_epg_filter_changed": self.on_epg_filter_changed,
"on_timers_press": self.on_timers_press, "on_timers_press": self.on_timers_press,
"on_timers_drag_data_received": self.on_timers_drag_data_received} "on_timers_drag_data_received": self.on_timers_drag_data_received,
"on_recordings_press": self.on_recordings_press,
"on_recording_filter_changed": self.on_recording_filter_changed,
"on_recordings_dir_changed": self.on_recordings_dir_changed}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers) builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
@@ -183,6 +256,11 @@ class ControlBox(Gtk.HBox):
# DnD initialization for the timer list. # DnD initialization for the timer list.
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY) self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._timers_list_box.drag_dest_add_text_targets() self._timers_list_box.drag_dest_add_text_targets()
# Recordings.
self._recordings_list_box = builder.get_object("recordings_list_box")
self._recordings_list_box.set_filter_func(self.recording_filter_function)
self._recordings_filter_entry = builder.get_object("recordings_filter_entry")
self._recordings_dir_box = builder.get_object("recordings_dir_box")
self.init_actions(app) self.init_actions(app)
self.connect("hide", self.on_hide) self.connect("hide", self.on_hide)
@@ -220,6 +298,8 @@ class ControlBox(Gtk.HBox):
app.set_action("on_timer_cancel", self.on_timer_cancel) app.set_action("on_timer_cancel", self.on_timer_cancel)
app.set_action("on_timer_begins_set", self.on_timer_begins_set) app.set_action("on_timer_begins_set", self.on_timer_begins_set)
app.set_action("on_timer_ends_set", self.on_timer_ends_set) app.set_action("on_timer_ends_set", self.on_timer_ends_set)
# Recordings
app.set_action("on_recording_remove", self.on_recording_remove)
@property @property
def update_epg(self): def update_epg(self):
@@ -232,6 +312,9 @@ class ControlBox(Gtk.HBox):
if tool is self.Tool.TIMERS: if tool is self.Tool.TIMERS:
self.update_timer_list() self.update_timer_list()
if tool is self.Tool.RECORDINGS:
self.update_recordings_list()
if tool is not self.Tool.TIMER: if tool is not self.Tool.TIMER:
self._last_tool = tool self._last_tool = tool
@@ -678,3 +761,66 @@ class ControlBox(Gtk.HBox):
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":")) self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
self.on_timer_add() self.on_timer_add()
context.finish(True, False, time) context.finish(True, False, time)
# *********************** Recordings *************************** #
def on_recordings_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self._http_api.send(HttpAPI.Request.STREAM_TS,
row.movie.get("e2filename", ""),
self.on_play_recording)
def on_recording_filter_changed(self, entry):
self._recordings_list_box.invalidate_filter()
def recording_filter_function(self, row):
txt = self._recordings_filter_entry.get_text().upper()
return any((not txt, txt in row.service.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def on_recording_remove(self, action, value=None):
""" Removes recordings via FTP. """
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
rows = self._recordings_list_box.get_selected_rows()
if rows:
settings = self._app._settings
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
for r in rows:
resp = ftp.delete_file(r.file)
if resp.startswith("2"):
GLib.idle_add(self._recordings_list_box.remove, r)
else:
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=resp)
break
def on_recordings_dir_changed(self, box: Gtk.ComboBoxText):
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(box.get_active_id()), self.update_recordings_data)
def update_recordings_list(self):
if not len(self._recordings_dir_box.get_model()):
self._http_api.send(HttpAPI.Request.REC_CURRENT, "", self.update_current_rec_dir)
def update_current_rec_dir(self, current):
cur = current.get("e2location", None)
if cur:
self._recordings_dir_box.append(cur, cur)
self._http_api.send(HttpAPI.Request.REC_DIRS, "", self.update_rec_dirs)
def update_rec_dirs(self, dirs):
for d in dirs.get("rec_dirs", []):
self._recordings_dir_box.append(d, d)
@run_idle
def update_recordings_data(self, recordings):
list(map(self._recordings_list_box.remove, (r for r in self._recordings_list_box)))
list(map(lambda r: self._recordings_list_box.add(self.RecordingsRow(r)), recordings.get("recordings", [])))
def on_play_recording(self, m3u):
url = self._app.get_url_from_m3u(m3u)
if url:
self._app.play(url)

View File

@@ -2634,6 +2634,7 @@ class Application(Gtk.Application):
def update_state_on_full_screen(self, visible): def update_state_on_full_screen(self, visible):
self._main_data_box.set_visible(visible) self._main_data_box.set_visible(visible)
self._player_tool_bar.set_visible(visible) self._player_tool_bar.set_visible(visible)
self._control_box.set_visible(visible)
self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible()) self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible())
def on_main_window_state(self, window, event): def on_main_window_state(self, window, event):
@@ -2768,7 +2769,7 @@ class Application(Gtk.Application):
GLib.idle_add(self._player_box.set_visible, True) GLib.idle_add(self._player_box.set_visible, True)
GLib.idle_add(self._app_info_box.set_visible, False) GLib.idle_add(self._app_info_box.set_visible, False)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.watch) self._http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
def watch(self, data): def watch(self, data):
url = self.get_url_from_m3u(data) url = self.get_url_from_m3u(data)