diff --git a/app/connections.py b/app/connections.py index c5eada58..3b782fdd 100644 --- a/app/connections.py +++ b/app/connections.py @@ -510,6 +510,7 @@ class HttpAPI: INFO = "about" SIGNAL = "signal" STREAM = "stream.m3u?ref=" + STREAM_TS = "ts.m3u?file=" STREAM_CURRENT = "streamcurrent.m3u" CURRENT = "getcurrent" TEST = None @@ -531,6 +532,10 @@ class HttpAPI: # Timer TIMER = "" TIMER_LIST = "timerlist" + # Recordings + RECORDINGS = "movielist?dirname=" + REC_DIRS = "getlocations" + REC_CURRENT = "getcurrlocation" # Screenshot GRUB = "grab?format=jpg&" @@ -557,6 +562,17 @@ class HttpAPI: WAKEUP = "4" 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): from concurrent.futures import ThreadPoolExecutor as PoolExecutor self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS) @@ -577,18 +593,14 @@ class HttpAPI: url = self._base_url + req_type.value 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) 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")) elif req_type is self.Request.GRUB: data = None # Must be disabled for token-based security. url = "{}/{}{}".format(self._main_url, req_type.value, ref) - elif req_type in (self.Request.REMOTE, - self.Request.POWER, - self.Request.VOL, - self.Request.EPG, - self.Request.TIMER): + elif req_type in self.PARAM_REQUESTS: url += ref def done_callback(f): @@ -632,7 +644,7 @@ class HttpAPI: def get_response(req_type, url, data=None): try: 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")} elif req_type is HttpAPI.Request.GRUB: 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: return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in 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: return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()} except HTTPError as e: diff --git a/app/ui/control.glade b/app/ui/control.glade index 6a8e3118..e465dbc8 100644 --- a/app/ui/control.glade +++ b/app/ui/control.glade @@ -325,6 +325,11 @@ Author: Dmitriy Yefremov view-refresh 1 + + True + False + user-trash + True False @@ -1721,6 +1726,103 @@ audio-volume-medium-symbolic 3 + + + True + False + vertical + 5 + + + True + True + in + + + True + False + + + True + False + multiple + False + + + + + + + + True + True + 0 + + + + + True + False + + + + False + True + 1 + + + + + True + False + 5 + + + True + True + tools-check-spelling + False + False + Filter + + + + True + True + 0 + + + + + True + True + True + Remove + app.on_recording_remove + remove_recording_image + True + + + False + True + end + 2 + + + + + False + False + 2 + + + + + recordings + Recordings + 4 + + True diff --git a/app/ui/control.py b/app/ui/control.py index 83bccc64..c18a78fb 100644 --- a/app/ui/control.py +++ b/app/ui/control.py @@ -9,7 +9,7 @@ from gi.repository import GLib from .dialogs import show_dialog, DialogType, get_message, get_builder from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column 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 @@ -22,6 +22,7 @@ class ControlBox(Gtk.HBox): EPG = "epg" TIMERS = "timers" TIMER = "timer" + RECORDINGS = "recordings" class EpgRow(Gtk.ListBoxRow): def __init__(self, event: dict, **properties): @@ -111,6 +112,75 @@ class ControlBox(Gtk.HBox): EVENT = 1 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("{}".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("{}".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("{}".format(start_time.strftime("%A, %H:%M"))) + + time_label = Gtk.Label() + time_label.set_margin_top(5) + time_label.set_markup("{}".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): super().__init__(*args, **kwargs) @@ -127,7 +197,10 @@ class ControlBox(Gtk.HBox): "on_epg_press": self.on_epg_press, "on_epg_filter_changed": self.on_epg_filter_changed, "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) @@ -183,6 +256,11 @@ class ControlBox(Gtk.HBox): # 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_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.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_begins_set", self.on_timer_begins_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 def update_epg(self): @@ -232,6 +312,9 @@ class ControlBox(Gtk.HBox): if tool is self.Tool.TIMERS: self.update_timer_list() + if tool is self.Tool.RECORDINGS: + self.update_recordings_list() + if tool is not self.Tool.TIMER: 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.on_timer_add() 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) diff --git a/app/ui/main_app_window.py b/app/ui/main_app_window.py index a4827d2d..2c7a023e 100644 --- a/app/ui/main_app_window.py +++ b/app/ui/main_app_window.py @@ -2634,6 +2634,7 @@ class Application(Gtk.Application): def update_state_on_full_screen(self, visible): self._main_data_box.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()) 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._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): url = self.get_url_from_m3u(data)