added recordings page

This commit is contained in:
DYefremov
2021-09-02 12:20:29 +03:00
parent 9fc07308ab
commit cb8ec264cc
4 changed files with 427 additions and 93 deletions

View File

@@ -121,7 +121,6 @@ Author: Dmitriy Yefremov
<object class="GtkTreeView" id="epg_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="model">epg_model</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">2</property>
@@ -1058,100 +1057,301 @@ Author: Dmitriy Yefremov
</packing>
</child>
</object>
<object class="GtkBox" id="recordings_box">
<object class="GtkListStore" id="rec_paths_model">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name path -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="recordings_model">
<columns>
<!-- column-name service -->
<column type="gchararray"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name time -->
<column type="gchararray"/>
<!-- column-name length -->
<column type="gchararray"/>
<!-- column-name file -->
<column type="gchararray"/>
<!-- column-name desc -->
<column type="gchararray"/>
<!-- column-name data -->
<column type="PyObject"/>
</columns>
</object>
<object class="GtkFrame" id="recordings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="margin_top">2</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</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">
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkSearchEntry" id="recordings_filter_entry">
<object class="GtkBox" id="recordings_header_box">
<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="always_show_image">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="remove_recording_image">
<object class="GtkButton" id="recordings_remove_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSearchEntry" id="recordings_filter_entry">
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">Filter</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="recordings_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</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="GtkTreeView" id="recordings_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">recordings_model</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">5</property>
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="recordings_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_service_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_service_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_title_column">
<property name="sizing">autosize</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_title_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_time_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_time_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_len_column">
<property name="min_width">100</property>
<property name="title" translatable="yes">Length</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_len_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_file_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">File</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_file_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_desc_column">
<property name="resizable">True</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="rec_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
<property name="width_request">250</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">100</property>
<child>
<object class="GtkTreeView" id="recordings_paths_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">rec_paths_model</property>
<property name="search_column">1</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="rec_paths_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_paths_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Paths</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child type="label">
<object class="GtkLabel" id="recordings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Recordings</property>
</object>
</child>
</object>
<object class="GtkListStore" id="timer_model">
@@ -1260,7 +1460,6 @@ Author: Dmitriy Yefremov
<object class="GtkTreeView" id="timer_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="model">timer_model</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">3</property>

View File

@@ -29,14 +29,15 @@
""" Receiver control module via HTTP API. """
import os
from datetime import datetime
from ftplib import all_errors
from urllib.parse import quote
from gi.repository import GLib
from .dialogs import get_builder
from .dialogs import get_builder, show_dialog, DialogType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
from ..connections import HttpAPI, UtfFTP
class EpgBox(Gtk.Box):
@@ -51,7 +52,7 @@ class EpgBox(Gtk.Box):
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, objects=("epg_frame", "epg_model"))
self._view = builder.get_object("epg_view")
self.add(builder.get_object("epg_frame"))
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
self.show()
def on_epg_press(self, list_box, event):
@@ -96,7 +97,7 @@ class TimersBox(Gtk.Box):
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, objects=("timers_frame", "timer_model"))
self._view = builder.get_object("timer_view")
self._remove_button = builder.get_object("timer_remove_button")
self.add(builder.get_object("timers_frame"))
self.pack_start(builder.get_object("timers_frame"), True, True, 0)
self.show()
def update_timer_list(self, app, page):
@@ -123,7 +124,143 @@ class TimersBox(Gtk.Box):
return name, service, time, description, timer
class ControlBox(Gtk.HBox):
class RecordingsBox(Gtk.Box):
ROOT = ".."
DEFAULT_PATH = "/hdd"
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._http_api = http_api
self._app = app
self._app.connect("profile-changed", self.init)
self._settings = settings
self._ftp = None
# Icon.
theme = Gtk.IconTheme.get_default()
icon = "folder-symbolic"
self._icon = theme.load_icon(icon, 32, 0) if theme.lookup_icon(icon, 32, 0) else None
handlers = {"on_path_press": self.on_path_press,
"on_path_activated": self.on_path_activated,
"on_recordings_activated": self.on_recordings_activated,
"on_recording_remove": self.on_recording_remove}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("recordings_frame", "recordings_model", "rec_paths_model"))
self._rec_view = builder.get_object("recordings_view")
self._paths_view = builder.get_object("recordings_paths_view")
self.pack_start(builder.get_object("recordings_frame"), True, True, 0)
self.init()
self.show()
def clear_data(self):
self._rec_view.get_model().clear()
self._paths_view.get_model().clear()
@run_task
def init(self, app=None, arg=None):
GLib.idle_add(self.clear_data)
try:
if self._ftp:
self._ftp.close()
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
self._ftp.encoding = "utf-8"
except all_errors:
pass # NOP
else:
self.init_paths(self.DEFAULT_PATH)
@run_idle
def init_paths(self, path=None):
self.clear_data()
if not self._ftp:
return
if path:
try:
self._ftp.cwd(path)
except all_errors as e:
pass
files = []
try:
self._ftp.dir(files.append)
except all_errors as e:
log(e)
else:
self.append_paths(files)
@run_idle
def append_paths(self, files):
model = self._paths_view.get_model()
model.clear()
model.append((None, self.ROOT, self._ftp.pwd()))
for f in files:
f_data = f.split()
f_type = f_data[0][0]
if f_type == "d":
model.append((self._icon, f_data[-1], self._ftp.pwd()))
def on_path_activated(self, view, path, column):
row = view.get_model()[path][:]
path = "{}/{}".format(row[-1], row[1])
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
def on_path_press(self, view, event):
target = view.get_path_at_pos(event.x, event.y)
if not target or event.button != Gdk.BUTTON_PRIMARY:
return
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.init_paths(self._paths_view.get_model()[target[0]][1])
@run_idle
def update_recordings_data(self, recordings):
model = self._rec_view.get_model()
model.clear()
list(map(model.append, (self.get_recordings_row(r) for r in recordings.get("recordings", []))))
def get_recordings_row(self, rec):
service = rec.get("e2servicename")
title = rec.get("e2title", "")
time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%A, %H:%M")
length = rec.get("e2length", "0")
file = rec.get("e2filename", "")
desc = rec.get("e2description", "")
return service, title, time, length, file, desc, rec
def on_recordings_activated(self, view, path, column):
rec = view.get_model()[path][-1]
self._http_api.send(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
def on_play_recording(self, m3u):
url = self._app.get_url_from_m3u(m3u)
if url:
self._app.play(url)
def on_recording_remove(self, action, value=None):
""" Removes recordings via FTP. """
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._rec_view.get_selection().get_selected_rows()
if paths and self._ftp:
for file, itr in ((model[p][-1].get("e2filename", ""), model.get_iter(p)) for p in paths):
resp = self._ftp.delete_file(file)
if resp.startswith("2"):
GLib.idle_add(model.remove, itr)
else:
self._app.show_error_message(resp)
break
class ControlBox(Gtk.Box):
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -137,7 +274,7 @@ class ControlBox(Gtk.HBox):
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("control_box", "volume_adjustment"))
self.add(builder.get_object("control_box"))
self.pack_start(builder.get_object("control_box"), True, True, 0)
self._stack = builder.get_object("stack")
self._screenshot_image = builder.get_object("screenshot_image")
self._screenshot_button_box = builder.get_object("screenshot_button_box")

View File

@@ -1480,7 +1480,6 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
@@ -2160,7 +2159,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_satellites_realize" swapped="no"/>
@@ -2179,7 +2177,6 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_picons_realize" swapped="no"/>
@@ -2385,7 +2382,6 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_epg_realize" swapped="no"/>
@@ -2404,7 +2400,6 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_timers_realize" swapped="no"/>
@@ -2424,9 +2419,9 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_recordings_realize" swapped="no"/>
<child>
<placeholder/>
</child>
@@ -2443,7 +2438,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_ftp_realize" swapped="no"/>
@@ -2463,7 +2457,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<signal name="realize" handler="on_control_realize" swapped="no"/>
<child>
@@ -2486,7 +2479,6 @@ Author: Dmitriy Yefremov
<object class="GtkPaned" id="fav_paned">
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="wide_handle">True</property>
<child>

View File

@@ -48,7 +48,7 @@ from app.eparser.neutrino.bouquets import BqType
from app.settings import (SettingsType, Settings, SettingsException, PlayStreamsMode, SettingsReadException,
IS_DARWIN)
from app.tools.media import Player, Recorder
from app.ui.control import ControlBox, EpgBox, TimersBox
from app.ui.control import ControlBox, EpgBox, TimersBox, RecordingsBox
from app.ui.epg_dialog import EpgDialog
from app.ui.ftp import FtpClientBox
from app.ui.transmitter import LinksTransmitter
@@ -201,6 +201,7 @@ class Application(Gtk.Application):
"on_picons_realize": self.on_picons_realize,
"on_epg_realize": self.on_epg_realize,
"on_timers_realize": self.on_timers_realize,
"on_recordings_realize": self.on_recordings_realize,
"on_control_realize": self.on_control_realize,
"on_ftp_realize": self.on_ftp_realize,
"on_visible_page": self.on_visible_page}
@@ -236,6 +237,7 @@ class Application(Gtk.Application):
self._picon_manager = None
self._epg_box = None
self._timers_box = None
self._recordings_box = None
self._control_box = None
self._ftp_client = None
# Player
@@ -743,6 +745,10 @@ class Application(Gtk.Application):
self._epg_box = TimersBox(self, self._http_api)
box.pack_start(self._epg_box, True, True, 0)
def on_recordings_realize(self, box):
self._recordings_box = RecordingsBox(self, self._http_api, self._settings)
box.pack_start(self._recordings_box, True, True, 0)
def on_ftp_realize(self, box):
self._ftp_client = FtpClientBox(self, self._settings)
box.pack_start(self._ftp_client, True, True, 0)