Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:ahmedmoselhi2
demoneditor
_service:obs_scm:demoneditor-extensions.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File _service:obs_scm:demoneditor-extensions.obscpio of Package demoneditor
07070100000000000081A400000000000000000000000166D18CCB00000022000000000000000000000000000000000000002600000000demoneditor-extensions/.gitattributesextensions/example/ export-ignore 07070100000001000081A400000000000000000000000166D18CCB00000005000000000000000000000000000000000000002200000000demoneditor-extensions/.gitignore.idea07070100000002000081A400000000000000000000000166D18CCB00000431000000000000000000000000000000000000001F00000000demoneditor-extensions/LICENSEMIT License Copyright (c) 2023 Dmitriy Yefremov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 07070100000003000081A400000000000000000000000166D18CCB00000234000000000000000000000000000000000000002100000000demoneditor-extensions/README.md# demoneditor-extensions ### Additional extensions for [DemonEditor](https://github.com/DYefremov/DemonEditor). ## Installation Just put your packages in one of these paths: ``` app/ui/extensions```, ``` your data path/tools/extensions ```. For builds: ``` Program Root\extensions ``` ## License Licensed under the [MIT](LICENSE) license. THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY. AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE. 07070100000004000041ED00000000000000000000000966D18CCB00000000000000000000000000000000000000000000002200000000demoneditor-extensions/extensions07070100000005000081A400000000000000000000000166D18CCB00000DF3000000000000000000000000000000000000002E00000000demoneditor-extensions/extensions/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # import json import logging import os from pathlib import Path CONFIG_PATH = f"{Path.home()}{os.sep}.config{os.sep}demon-editor{os.sep}extensions{os.sep}" class Singleton(type): _INSTANCE = None def __call__(cls, *args, **kwargs): if not cls._INSTANCE: cls._INSTANCE = type.__call__(cls, *args, **kwargs) return cls._INSTANCE class BaseExtension(metaclass=Singleton): """ Base extension (plugin) class. """ # The label that will be displayed in the "Tools" menu. LABEL = "Base extension" VERSION = "1.0" # Additional flags. EMBEDDED = False SWITCHABLE = False _LOGGER_NAME = "main_logger" def __init__(self, app): # Current application instance. # It can be used all public methods, properties or signals. # Main app class -> https://github.com/DYefremov/DemonEditor/blob/master/app/ui/main.py self.app = app self._config_path = f"{CONFIG_PATH}{self.__class__.__name__}{os.sep}config" self.log(f"Extension initialized...") def exec(self): """ Triggers an action for the given extension. E.g. shows a dialog or runs an external script. """ self.app.show_info_message(f"Hello from {self.__class__.__name__} class!") def stop(self): """ Stops (terminates) the task or the extension itself. """ self.log("Terminating a task...") def log(self, message, level=logging.ERROR): """ Shows log messages. """ logging.getLogger(self._LOGGER_NAME).log(level, f"[{self.__class__.__name__}] {message}") def reset_config(self): path = Path(self._config_path) if path.is_file(): path.unlink() @property def config(self) -> dict: if not Path(self._config_path).is_file(): return {} with open(self._config_path, "r", encoding="utf-8") as config_file: try: return json.load(config_file) except ValueError as e: self.log(f"Configuration load error: {e}") return {} @config.setter def config(self, value: dict): Path(self._config_path).parent.mkdir(parents=True, exist_ok=True) with open(self._config_path, "w", encoding="utf-8") as config_file: json.dump(value, config_file, indent=" ") if __name__ == "__main__": pass 07070100000006000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000002C00000000demoneditor-extensions/extensions/epgexport07070100000007000081A400000000000000000000000166D18CCB000000BC000000000000000000000000000000000000003600000000demoneditor-extensions/extensions/epgexport/README.md## EPG export Allows you to export XMLTV data as an ```epg.dat``` file from the EPG tab to the receiver. ### Requirements [DemonEditor](https://github.com/DYefremov/DemonEditor) >= 3.9.007070100000008000081A400000000000000000000000166D18CCB0000457A000000000000000000000000000000000000003800000000demoneditor-extensions/extensions/epgexport/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2024 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # import os import struct from datetime import datetime from io import BytesIO from itertools import chain from shutil import copyfileobj from gi.repository import GLib from app.connections import UtfFTP from app.ui.dialogs import show_dialog, DialogType from app.ui.tasks import BGTaskWidget from app.ui.uicommons import Page, Column, Gtk from extensions import BaseExtension class Epgexport(BaseExtension): LABEL = "EPG Export" EMBEDDED = True VERSION = "1.0" def __init__(self, app): super().__init__(app) self._f_name = "epg.dat" self._cache = None self._xmltv_button = None self._send_on_done = True # Checking for the required version. if not hasattr(app, "DATA_SEND_PAGES"): msg = "Init error. Minimum required app version >= 3.9.0." self.log(msg) self.app.show_error_message(f"[{self.__class__.__name__}] {msg}") return app.DATA_SEND_PAGES.add(Page.EPG) app._stack_epg_box.connect("realize", self.on_epg_tab_realize) app.connect("data-send", self.on_data_send) def on_epg_tab_realize(self, widget): from app.ui.epg.epg import EpgTool children = widget.get_children() if not children or not isinstance(children[0], EpgTool): self.log("Initialization error [EPG tab].") return tool = children[0] self._xmltv_button = tool._src_xmltv_button self.app.connect("epg-cache-initialized", self.on_cache_init) self.app.connect("epg-cache-updated", self.on_cache_init) def on_cache_init(self, tool, cache): from app.ui.epg.epg import TabEpgCache if isinstance(cache, TabEpgCache): self._cache = cache def on_data_send(self, app, page): if page is not Page.EPG: return if self._xmltv_button.get_active(): self.send_data() else: self.app.show_error_message("Load XML TV data first!") def send_data(self): if not self._cache: self.app.show_error_message("Error. EPG cache is not initialized!") self.log("EPG cache is not initialized!") return msg = ("\n\t This operation will overwrite\n" "the current EPG cache file of your receiver!\n\n\t\t\tAre you sure?\n\n\n" "\t\t\t\tATTENTION\n" "After the operation is completed, load the cache data\n" " manually using the receiver menu!") if show_dialog(DialogType.QUESTION, self.app.app_window, msg) != Gtk.ResponseType.OK: return self.app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True)) self.log("Checking bouquets selection...") current = self.app.current_bouquets model, paths = self.app.bouquets_view.get_selection().get_selected_rows() selected_bouquets = current.keys() & {f"{model[p][Column.BQ_NAME]}:{model[p][Column.BQ_TYPE]}" for p in paths} self.log(f"Selected bouquets: {len(selected_bouquets)}") if not selected_bouquets: self.app.show_error_message("Error. No bouquet is selected!") return services = self.app.current_services events = self._cache.events selected_services = (services[s] for s in chain.from_iterable(current[b] for b in selected_bouquets)) services = [(s, events[s.service]) for s in selected_services if s.service in events] self.log(f"Services with EPG in cache: {len(services)}") # Processing data. path = f"{self.app.app_settings.profile_data_path}epg{os.sep}" self.process_dat(path, services) def process_dat(self, path, services): msg = f"Creating '{self._f_name}' file..." self.log(msg) writer = EpgWriter(f"{path}{self._f_name}", services, self.log) def process(): writer.write() if self._send_on_done: self.send_dat(path) task = BGTaskWidget(self.app, msg, process, ) self.app.emit("add-background-task", task) def send_dat(self, path): settings = self.app.app_settings with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: ftp.encoding = "utf-8" try: self.log(f"Current dir for '{self._f_name}': {settings.epg_dat_path}") ftp.cwd(settings.epg_dat_path) except Exception as e: self.log(e) else: resp = ftp.send_file(self._f_name, path, self.log) if resp.startswith("2"): self.app.show_info_message("Done!", Gtk.MessageType.INFO) class EpgWriter: """ The epd.dat file writing class. The base writing code and algorithm was taken from the 'epgdat.py' file of EPGImport plugin from here: https://github.com/OpenPLi/enigma2-plugin-extensions-epgimport """ # This table is used by CRC32 routine below (used by Dreambox for computing REF DESC value). # The original DM routine is a modified CRC32 standard routine, so cannot use Python standard binascii.crc32() CRC_TABLE = (0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005, 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9, 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75, 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD, 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81, 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D, 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95, 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE, 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072, 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA, 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066, 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA, 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692, 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E, 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A, 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F, 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53, 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B, 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7, 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B, 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3, 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F, 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3, 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C, 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30, 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC, 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654, 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18, 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C, 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4) LB_ENDIAN = '<' EPG_PROLEPTIC_ZERO_DAY = 678576 def __init__(self, path, services, log_func=print): self.epg_dat_path = path self._services = services self.header1_srv_count = 0 self.header2_desc_count = 0 self.total_events = 0 self._events = {} self.s_BB = struct.Struct("BB") self.s_BBB = struct.Struct("BBB") self.s_b_HH = struct.Struct(">HH") self.s_I = struct.Struct(self.LB_ENDIAN + "I") self.s_II = struct.Struct(self.LB_ENDIAN + "II") self.s_IIII = struct.Struct(self.LB_ENDIAN + "IIII") self.s_B3sHBB = struct.Struct("B3sHBB") self.s_B3sBBB = struct.Struct("B3sBBB") self.s_3sBB = struct.Struct("3sBB") self.log = log_func @staticmethod def get_crc32(crc_data, crc_type, crc_table=CRC_TABLE): """ CRC32 in Dreambox/DVB way (see CRC_TABLE). :param crc_data: description string :param crc_type: description type (1 byte 0x4d or 0x4e) :param crc_table: -> CRC_TABLE """ crc = crc_table[crc_type & 0x000000ff] crc = ((crc << 8) & 0xffffff00) ^ crc_table[((crc >> 24) ^ len(crc_data)) & 0x000000ff] for d in crc_data: crc = ((crc << 8) & 0xffffff00) ^ crc_table[((crc >> 24) ^ d) & 0x000000ff] return crc @staticmethod def get_tl_hexconv(dt): return ((dt.hour % 10) + (16 * (dt.hour // 10)), (dt.minute % 10) + (16 * (dt.minute // 10)), (dt.second % 10) + (16 * (dt.second // 10))) def short_desc(self, desc): """ Assembling short description (type 0x4d , it's the Title) and compute its crc. 0x15 -> UTF-8 encoding. """ b_desc = desc.encode(encoding="utf-8", errors="ignore")[:240] b_data = self.s_3sBB.pack(b"eng", len(b_desc) + 1, 0x15) + b_desc + b"\0" return self.get_crc32(b_data, 0x4d), b_data def long_desc(self, desc): """ Assembling long description (type 0x4e) and compute its crc. Compute total number of descriptions, block 245 bytes each number of descriptions start to index 0 """ res = [] b_desc = desc.encode(encoding="utf-8", errors="ignore") # Maximum number of data chunks. Used for limit the description size. max_count = 15 desc_count = (len(b_desc) + 244) // 245 if desc_count > max_count: desc_count = max_count for i in range(desc_count): ssub = b_desc[i * 245:i * 245 + 245] if i < max_count else b"..." b_data = self.s_B3sBBB.pack((i << 4) + (desc_count - 1), b"eng", 0x00, int(len(ssub) + 1), 0x15) + ssub res.append((self.get_crc32(b_data, 0x4e), b_data)) return res def get_event(self, ed): title = ed.get("e2eventtitle", "") desc = ed.get("e2eventdescription", None) or title return ed.get("e2eventstart"), ed.get("e2eventduration"), self.short_desc(title), self.long_desc(desc) def write(self): epg_event_data_id = 0 default_iptv_ref_detected = False def_ref_data = ("0", "0", "0") with BytesIO() as tf: for srv, ev in self._services: # sid, nid, tid. sd = srv.fav_id.split(":") if len(sd) == 4: sid, nid, tid, _ = sd else: sid, nid, tid = sd[3], sd[5], sd[4] if (sid, nid, tid) == def_ref_data: # Detecting IPTV services with default values of SID, NIT, TID. if not default_iptv_ref_detected: self.log("Detected IPTV service(s) with values for [SID, NID, TID] = 0. Skipping...") default_iptv_ref_detected = True continue try: sid, nid, tid = int(sid, 16), int(nid, 16), int(tid, 16) except ValueError as e: self.log(f"Getting service [{srv.service}] data error: {e}") continue events = [self.get_event(d) for d in (e.event_data for e in ev)] tf.write(self.s_IIII.pack(sid, nid, tid, len(events))) self.header1_srv_count += 1 s_bb = self.s_BB s_bbb = self.s_BBB s_i = self.s_I for event in events: # **** (1) : create DESCRIPTION HEADER / DATA **** event_header_size = 0 # Short description (title) [type 0x4d]. short_desc = event[2] event_header_size += 4 # add 4 bytes for a single REF DESC (CRC32) if short_desc[0] not in self._events: # DESCRIPTION DATA self._events[short_desc[0]] = [s_bb.pack(0x4d, len(short_desc[1])) + short_desc[1], 1] self.header2_desc_count += 1 else: self._events[short_desc[0]][1] += 1 # Long description [type 0x4e]. long_desc = event[3] event_header_size += 4 * len(long_desc) # add 4 bytes for a single REF DESC (CRC32) for desc in long_desc: if desc[0] not in self._events: # DESCRIPTION DATA self._events[desc[0]] = [s_bb.pack(0x4e, len(desc[1])) + desc[1], 1] self.header2_desc_count += 1 else: self._events[desc[0]][1] += 1 # EVENT HEADER (3 bytes: 0x01 , 0x00, 10 bytes + number of CRC32 * 4) tf.write(s_bbb.pack(0x01, 0x00, 0x0a + event_header_size)) # Time. event_time_hms = datetime.utcfromtimestamp(event[0]) event_length_hms = datetime.utcfromtimestamp(event[1]) dvb_date = event_time_hms.toordinal() - self.EPG_PROLEPTIC_ZERO_DAY # EVENT DATA epg_event_data_id += 1 pack_1 = self.s_b_HH.pack(epg_event_data_id, dvb_date) # ID and DATE , always in BIG_ENDIAN pack_2 = s_bbb.pack(*self.get_tl_hexconv(event_time_hms)) # Start pack_3 = s_bbb.pack(*self.get_tl_hexconv(event_length_hms)) # Duration pack_4 = s_i.pack(short_desc[0]) # Short description (title). for d in long_desc: pack_4 += s_i.pack(d[0]) # REF DESC long tf.write(pack_1 + pack_2 + pack_3 + pack_4) if len(self._events) > 0: self.finalize(tf) def finalize(self, header_data): with open(self.epg_dat_path, "wb") as dat_fd: # HEADER 1. pack_1 = struct.pack(self.LB_ENDIAN + "I13sI", 0x98765432, b'ENIGMA_EPG_V8', self.header1_srv_count) dat_fd.write(pack_1) # Write first EPG.DAT section. header_data.seek(0) copyfileobj(header_data, dat_fd) # HEADER 2 s_ii = self.s_II pack_1 = self.s_I.pack(self.header2_desc_count) dat_fd.write(pack_1) # Event MUST BE WRITTEN IN ASCENDING ORDERED using HASH CODE as index. for temp in sorted(self._events.keys()): pack_2 = self._events[temp] pack_1 = s_ii.pack(temp, pack_2[1]) dat_fd.write(pack_1 + pack_2[0]) self.log("The 'epg.dat' file creation is complete.") if __name__ == "__main__": pass 07070100000009000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000002A00000000demoneditor-extensions/extensions/example0707010000000A000081A400000000000000000000000166D18CCB000008E8000000000000000000000000000000000000003600000000demoneditor-extensions/extensions/example/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # from gi.repository import Gtk from extensions import BaseExtension class Example(BaseExtension): """ An example of a simple extension class. """ LABEL = "Example extension" def __init__(self, app): super().__init__(app) self._window = Gtk.Window(title=self.LABEL, destroy_with_parent=True, modal=True) self._window.set_default_size(320, 140) self._window.set_transient_for(app.app_window) self._window.set_application(app) self._window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) button = Gtk.Button("Show message!", visible=True) button.connect("clicked", self.on_show_message) self._window.add(button) self._window.connect("delete-event", self.on_destroy) def exec(self): self._window.show() def on_show_message(self, button): self.app.show_info_message("Hello from 'Example' extension!") def on_destroy(self, window, event): """ Used to hide window instead of destroying. """ window.hide() return True if __name__ == '__main__': pass 0707010000000B000081A400000000000000000000000166D18CCB000004BF000000000000000000000000000000000000003100000000demoneditor-extensions/extensions/extension-list{ "epgexport": { "label": "EPG Export", "description": "Allows you to export XMLTV data as an epg.dat file from the EPG tab to the receiver.", "version": "1.0", "embedded": true, "switchable": false, "ref": "epgexport?ref=main" }, "filterhighlight": { "label": "Filter highlight", "description": "Marks (highlights) filtered text.", "version": "1.1", "embedded": true, "switchable": false, "ref": "filterhighlight?ref=main" }, "iptvcleanup": { "label": "IPTV bouquets cleanup", "description": "Extension for searching and removing IPTV channels with duplicate URLs.", "version": "1.0", "embedded": false, "switchable": false, "ref": "iptvcleanup?ref=main" }, "oscamstatus": { "label": "OSCam status", "description": "A simple extension to display OSCam status.", "version": "1.0", "embedded": false, "switchable": false, "ref": "oscamstatus?ref=main" }, "streamimport": { "label": "Advanced streams import", "description": "Allows you to import IPTV streams from various sources.", "version": "1.1", "embedded": false, "switchable": false, "ref": "streamimport?ref=main" } } 0707010000000C000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000003000000000demoneditor-extensions/extensions/favautomarker0707010000000D000081A400000000000000000000000166D18CCB000000BE000000000000000000000000000000000000003A00000000demoneditor-extensions/extensions/favautomarker/README.md## Mark not in bouquets Automatically marks in the main list services that are not presented in bouquets. ### Requirements [DemonEditor](https://github.com/DYefremov/DemonEditor) >= 3.11.00707010000000E000081A400000000000000000000000166D18CCB00000838000000000000000000000000000000000000003C00000000demoneditor-extensions/extensions/favautomarker/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2024 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # from extensions import BaseExtension from gi.repository import GLib class Favautomarker(BaseExtension): LABEL = "Mark not in bouquets" EMBEDDED = True VERSION = "1.0" def __init__(self, app): super().__init__(app) if not hasattr(app, "VERSION") or app.VERSION < "3.11.0": msg = "Init error. Minimum required app version >= 3.11.0." self.log(msg) self.app.show_error_message(f"[{self.__class__.__name__}] {msg}") return app.connect("fav-added", self.update_mark) app.connect("fav-removed", self.update_mark) app.connect("bouquet-removed", self.update_mark) app.connect("data-load-done", self.update_mark) def update_mark(self, app, data=None): gen = self.app.mark_not_in_bouquets() GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) if __name__ == "__main__": pass 0707010000000F000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000003200000000demoneditor-extensions/extensions/filterhighlight07070100000010000081A400000000000000000000000166D18CCB0000108C000000000000000000000000000000000000003E00000000demoneditor-extensions/extensions/filterhighlight/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # import re from gi.repository import Gtk from app.ui.uicommons import Column from extensions import BaseExtension class Filterhighlight(BaseExtension): LABEL = "Filter highlight" EMBEDDED = True VERSION = "1.1" def __init__(self, app): super().__init__(app) style_context = self.app.services_view.get_style_context() self._color = style_context.get_color(Gtk.StateFlags.LINK).to_color().to_string() app._stack_services_frame.connect("realize", self.on_bq_tab_realize) app._stack_epg_box.connect("realize", self.on_epg_tab_realize) app._stack_recordings_box.connect("realize", self.on_recordings_tab_realize) def on_bq_tab_realize(self, widget): # Sat sat_entry = self.app._filter_entry view = self.app.services_view self.init_data_func(view, 2, sat_entry, Column.SRV_SERVICE, 1) # Service self.init_data_func(view, 3, sat_entry, Column.SRV_PACKAGE, 0) # Package # IPTV iptv_entry = self.app._iptv_filter_entry view = self.app.iptv_services_view self.init_data_func(view, 0, iptv_entry, Column.IPTV_SERVICE, 0) # Service def on_epg_tab_realize(self, widget): from app.ui.epg.epg import EpgTool children = widget.get_children() if not children or not isinstance(children[0], EpgTool): self.log("Initialization error [EPG tab].") return tool = children[0] view = tool._view entry = tool._filter_entry self.init_data_func(view, 0, entry, 0, 0) # Service self.init_data_func(view, 1, entry, 1, 0) # Title self.init_data_func(view, 5, entry, 5, 0) # Description def on_recordings_tab_realize(self, widget): from app.ui.recordings import RecordingsTool children = widget.get_children() if not children or not isinstance(children[0], RecordingsTool): self.log("Initialization error [Recordings tab].") return tool = children[0] view = tool._rec_view entry = tool._filter_entry self.init_data_func(view, 0, entry, 1, 1) # Service self.init_data_func(view, 1, entry, 2, 0) # Title self.init_data_func(view, 5, entry, 6, 0) # Description def init_data_func(self, view, column, entry, model_column, cel): v_column = view.get_column(column) v_column.set_cell_data_func(v_column.get_cells()[cel], lambda c, r, m, i, d: self.data_func( model_column, r, m, i, entry.get_text().upper())) def data_func(self, column, renderer, model, itr, f_txt): txt = model[itr][column] if not txt: return True if f_txt and f_txt in txt.upper(): murk = re.sub(re.escape(f_txt), lambda m: f'<span foreground="{self._color}"><b><u>{m.group()}</u></b></span>', txt, flags=re.I) renderer.set_property("markup", murk) if __name__ == "__main__": pass 07070100000011000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000002E00000000demoneditor-extensions/extensions/iptvcleanup07070100000012000081A400000000000000000000000166D18CCB0000004B000000000000000000000000000000000000003800000000demoneditor-extensions/extensions/iptvcleanup/README.md### Extension for searching and removing IPTV channels with duplicate URLs.07070100000013000081A400000000000000000000000166D18CCB000010E1000000000000000000000000000000000000003A00000000demoneditor-extensions/extensions/iptvcleanup/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # from collections import defaultdict from gi.repository import Gtk, GLib from app.ui.main_helper import get_iptv_data from app.ui.uicommons import Column from extensions import BaseExtension class Iptvcleanup(BaseExtension): LABEL = "IPTV bouquets cleanup" class CleanupDialog(Gtk.Dialog): def __init__(self, data: dict, **kwargs): super().__init__(title="IPTV bouquets cleanup", default_width=320, **kwargs) area = self.get_content_area() label = Gtk.Label(margin_top=15, margin_bottom=15) count = 0 if not data.keys(): self.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) else: count = sum((len(x) for x in data.values())) self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_DELETE, Gtk.ResponseType.OK) label.set_text(f"Found: {count}") area.pack_start(label, True, True, 0) self.show_all() def __init__(self, app): super().__init__(app) def exec(self): if not self.app.is_enigma: self.app.show_error_message("Neutrino is not supported!") return view = self.app.bouquets_view model, paths = view.get_selection().get_selected_rows() if not paths: self.app.show_info_message("No selected item!") else: bqs = self.app.current_bouquets to_remove = defaultdict(set) for p in paths: bq_id = f"{model[p][Column.BQ_NAME]}:{model[p][Column.BQ_TYPE]}" bq = bqs.get(bq_id, None) if bq: exist = set() for i, s in enumerate(bq): res = get_iptv_data(s) if all(res): if res[-1] in exist: to_remove[bq_id].add(i) else: exist.add(res[-1]) dialog = self.CleanupDialog(to_remove, transient_for=self.app.app_window) if dialog.run() == Gtk.ResponseType.OK: bq_selected = self.app.check_bouquet_selection() count = 0 for b, indexes in to_remove.items(): if b == bq_selected: # Processing selected bouquet list if it is selected for cleaning. model = self.app.fav_view.get_model() to_remove = [row.iter for i, row in enumerate(model) if i in indexes] gen = self.app.remove_favs(to_remove, model) GLib.idle_add(lambda: next(gen, False)) count += len(to_remove) else: bq = bqs.get(b) bq_size = len(bq) [bq.pop(i) for i in sorted(indexes, reverse=True)] count += bq_size - len(bq) self.app.show_info_message(f"Done! Removed: {count}") dialog.destroy() if __name__ == "__main__": pass 07070100000014000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000002E00000000demoneditor-extensions/extensions/oscamstatus07070100000015000081A400000000000000000000000166D18CCB0000216B000000000000000000000000000000000000003A00000000demoneditor-extensions/extensions/oscamstatus/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # import json import os from enum import Enum from threading import Thread from urllib.request import (urlopen, Request, HTTPPasswordMgrWithDefaultRealm, HTTPDigestAuthHandler, build_opener, install_opener) from gi.repository import GLib, Gtk, GObject from extensions import BaseExtension class OSCRequest(Enum): READERS = "part=readerlist" def __str__(self): return self.value class Oscamstatus(BaseExtension): """ A simple extension to display OSCam status. """ LABEL = "OSCam status" def __init__(self, app): super().__init__(app) app.connect("profile-changed", self.on_profile_changed) # Settings. self._host = app.app_settings.host self._base_url = None self._url = None self._config = self.get_config() self.init_urls() _base_path = os.path.dirname(__file__) builder = Gtk.Builder.new_from_file(f"{_base_path}{os.sep}dialog.ui") # Settings. self._user_entry = builder.get_object("user_entry") self._password_entry = builder.get_object("password_entry") self._port_entry = builder.get_object("port_entry") self._refresh_button = builder.get_object("refresh_spin_button") builder.get_object("apply_config_button").connect("clicked", self.on_apply_config) self._window = builder.get_object("window") self._window.set_title(self.LABEL) refresh_interval = self._config.get('refresh_interval', 3) self._window.connect("show", lambda w: GLib.timeout_add_seconds(refresh_interval, self.update_status)) self._window.connect("delete-event", self.on_close_window) self._stack = builder.get_object("stack") self._stack.connect("notify::visible-child-name", self.on_page_changed) self._restart_button = builder.get_object("restart_button") self._restart_button.connect("clicked", self.on_restart) self._version_label = builder.get_object("version_label") self._readers_count_label = builder.get_object("readers_count_label") self._readers_view = builder.get_object("readers_view") GObject.signal_new("data-changed", self._readers_view, GObject.SIGNAL_RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) self._readers_view.connect("data-changed", self.on_data_changed) self._readers_view.connect("query-tooltip", self.on_readers_query_tooltip) model = self._readers_view.get_model() model.connect("row-deleted", self.on_readers_model_changed) model.connect("row-inserted", self.on_readers_model_changed) self.init_auth(self._base_url) def init_urls(self): self._base_url = f"http://{self._host}:{self._config.get('port', '8080')}/oscamapi.json?" self._url = f"{self._base_url}{OSCRequest.READERS}" def exec(self): self._window.show() def init_auth(self, url): pass_mgr = HTTPPasswordMgrWithDefaultRealm() pass_mgr.add_password(None, url, self._config.get("user", ""), self._config.get("password", "")) auth_handler = HTTPDigestAuthHandler(pass_mgr) opener = build_opener(auth_handler) install_opener(opener) def update_status(self): active = self._window.get_visible() if active: Thread(target=self.refresh_data, daemon=True).start() return active def on_profile_changed(self, app, prf): self._readers_view.get_model().clear() self._host = app.app_settings.host self.init_urls() self.init_auth(self._base_url) self.log("profile changed") def refresh_data(self): data = None try: with urlopen(Request(self._url, data=None), timeout=2) as resp: if resp.status == 200: data = json.load(resp) self.log("Update state") else: self.log(f"Error: {resp.status}") except (OSError, ValueError) as e: self.log(f"Error: {e}") GLib.idle_add(self._readers_view.emit, "data-changed", data) def on_data_changed(self, list_box, data): model = self._readers_view.get_model() if not data: model.clear() return readers = {row[-1]["label"]: row.iter for row in model} osc = data.get("oscam", None) if osc: self._version_label.set_text(osc.get("version", "N/A")) for r in osc.get("readers"): enabled = bool(int(r.get("enabled", 0))) reader = (r.get("label", None), r.get("protocol", None), enabled, r) itr = readers.pop(r.get("label"), None) if itr: model[itr][:] = reader else: model.append(reader) # Removal if some readers were not found during the update. [model.remove(i) for i in readers.values()] def on_readers_query_tooltip(self, view, x, y, keyboard_mode, tooltip): result = view.get_dest_row_at_pos(x, y) if not result: return False path, pos = result data = view.get_model()[path][-1] if data: stats = data.get("stats") tooltip.set_markup("\n".join(f'<span weight="bold">{str(k).title()}</span>: {v}' for k, v in stats.items())) view.set_tooltip_row(tooltip, path) return True return False def on_restart(self, button): url = f"http://{self._host}:{self._config.get('port', '8080')}/shutdown.html?action=Restart" self.init_auth(url) try: with urlopen(Request(url, data=None), timeout=2) as f: if f.status == 200: self.log("Restarting...") else: self.log(f"Error: {f.status}") except OSError as e: self.log(e) GLib.timeout_add_seconds(2, self.init_auth, self._base_url) def on_readers_model_changed(self, model, path, itr=None): self._readers_count_label.set_text(str(len(model))) def on_apply_config(self, button): self._config["user"] = self._user_entry.get_text() self._config["password"] = self._password_entry.get_text() self._config["port"] = self._port_entry.get_text() self._config["refresh_interval"] = self._refresh_button.get_value() self.config = self._config self.init_urls() self.init_auth(self._base_url) self._stack.set_visible_child_name("readers") def get_config(self): return self.config or {"port": '8080', "user": "oscam", "password": "oscam", "refresh_interval": 3} def on_page_changed(self, stack, param): if stack.get_visible_child_name() == "settings": self._user_entry.set_text(self._config.get("user", "oscam")) self._password_entry.set_text(self._config.get("password", "oscam")) self._port_entry.set_text(self._config.get("port", "8080")) self._refresh_button.set_value(self._config.get("refresh_interval", 3)) def on_close_window(self, window, event): """ Prevents window destroying when the close button is clicked. """ window.hide() return True if __name__ == '__main__': pass 07070100000016000081A400000000000000000000000166D18CCB00005B9E000000000000000000000000000000000000003800000000demoneditor-extensions/extensions/oscamstatus/dialog.ui<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.38.2 The MIT License (MIT) Copyright (c) Dmitriy Yefremov 2023 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Author: Dmitriy Yefremov --> <interface domain="oscam-status"> <requires lib="gtk+" version="3.22"/> <!-- interface-license-type mit --> <!-- interface-name OSCam status --> <!-- interface-description Extension for DemonEditor --> <!-- interface-copyright Dmitriy Yefremov 2023 --> <!-- interface-authors Dmitriy Yefremov --> <object class="GtkImage" id="apply_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">document-save-symbolic</property> </object> <object class="GtkListStore" id="readers_model"> <columns> <!-- column-name name --> <column type="gchararray"/> <!-- column-name protocol --> <column type="gchararray"/> <!-- column-name enabled --> <column type="gboolean"/> <!-- column-name data --> <column type="PyObject"/> </columns> </object> <object class="GtkAdjustment" id="refresh_adjustment"> <property name="lower">2</property> <property name="upper">10</property> <property name="value">2</property> <property name="step-increment">1</property> <property name="page-increment">10</property> </object> <object class="GtkWindow" id="window"> <property name="can-focus">False</property> <property name="modal">True</property> <property name="window-position">center</property> <property name="default-width">480</property> <property name="default-height">240</property> <property name="destroy-with-parent">True</property> <property name="icon-name">demon-editor</property> <property name="type-hint">dialog</property> <child> <object class="GtkBox" id="main_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="GtkBox" id="toolbar_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="spacing">5</property> <child> <object class="GtkStackSwitcher" id="stack_switcher"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">end</property> <property name="margin-start">5</property> <property name="margin-end">10</property> <property name="margin-top">5</property> <property name="margin-bottom">5</property> <property name="stack">stack</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="pack-type">end</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkBox" id="actions_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="valign">center</property> <property name="margin-start">10</property> <property name="spacing">5</property> <child> <object class="GtkButton" id="restart_button"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> <property name="tooltip-text" translatable="yes">Restart</property> <child> <object class="GtkImage" id="restart_button_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">view-refresh</property> </object> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <placeholder/> </child> <child> <placeholder/> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <style> <class name="primary-toolbar"/> </style> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkStack" id="stack"> <property name="visible">True</property> <property name="can-focus">False</property> <child> <object class="GtkBox" id="readers_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="readers_view_scrolled"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="margin-start">5</property> <property name="margin-end">5</property> <property name="shadow-type">in</property> <child> <object class="GtkTreeView" id="readers_view"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="model">readers_model</property> <property name="search-column">0</property> <property name="enable-grid-lines">both</property> <property name="tooltip-column">3</property> <property name="activate-on-single-click">True</property> <child internal-child="selection"> <object class="GtkTreeSelection"/> </child> <child> <object class="GtkTreeViewColumn" id="reader_name_column"> <property name="resizable">True</property> <property name="min-width">50</property> <property name="title" translatable="yes">Name</property> <property name="expand">True</property> <property name="alignment">0.5</property> <child> <object class="GtkCellRendererText" id="reader_name_renderer"> <property name="xpad">5</property> </object> <attributes> <attribute name="text">0</attribute> </attributes> </child> </object> </child> <child> <object class="GtkTreeViewColumn" id="reader_protocol_column"> <property name="resizable">True</property> <property name="min-width">50</property> <property name="title" translatable="yes">Protocol</property> <property name="expand">True</property> <property name="alignment">0.5</property> <child> <object class="GtkCellRendererText" id="reader_protocol_rendrer"> <property name="xalign">0.49000000953674316</property> </object> <attributes> <attribute name="text">1</attribute> </attributes> </child> </object> </child> <child> <object class="GtkTreeViewColumn" id="reader_enable_column"> <property name="fixed-width">100</property> <property name="min-width">50</property> <property name="title" translatable="yes">Enabled</property> <property name="alignment">0.5</property> <child> <object class="GtkCellRendererToggle" id="reader_enable_renderer"/> <attributes> <attribute name="active">2</attribute> </attributes> </child> </object> </child> </object> </child> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <placeholder/> </child> </object> <packing> <property name="name">readers</property> <property name="title" translatable="yes">Readers</property> </packing> </child> <child> <!-- n-columns=2 n-rows=5 --> <object class="GtkGrid" id="settings_grid"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">center</property> <property name="valign">center</property> <property name="margin-start">10</property> <property name="margin-end">10</property> <property name="margin-top">10</property> <property name="margin-bottom">10</property> <property name="row-spacing">5</property> <property name="column-spacing">5</property> <property name="column-homogeneous">True</property> <child> <object class="GtkLabel" id="user_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">User:</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">0</property> </packing> </child> <child> <object class="GtkLabel" id="password_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">Password:</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">1</property> </packing> </child> <child> <object class="GtkLabel" id="port_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">Port:</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">2</property> </packing> </child> <child> <object class="GtkEntry" id="user_entry"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="primary-icon-name">avatar-default-symbolic</property> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">0</property> </packing> </child> <child> <object class="GtkEntry" id="password_entry"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="visibility">False</property> <property name="invisible-char">●</property> <property name="primary-icon-name">emblem-readonly</property> <property name="input-purpose">password</property> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">1</property> </packing> </child> <child> <object class="GtkEntry" id="port_entry"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="halign">end</property> <property name="width-chars">8</property> <property name="xalign">1</property> <property name="primary-icon-name">network-workgroup-symbolic</property> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">2</property> </packing> </child> <child> <object class="GtkLabel" id="refresh_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">Refresh interval (sec.):</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">3</property> </packing> </child> <child> <object class="GtkSpinButton" id="refresh_spin_button"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="tooltip-text" translatable="yes">Restart required!</property> <property name="halign">end</property> <property name="max-length">5</property> <property name="width-chars">5</property> <property name="progress-pulse-step">1</property> <property name="primary-icon-name">alarm-symbolic</property> <property name="input-purpose">digits</property> <property name="adjustment">refresh_adjustment</property> <property name="climb-rate">1</property> <property name="numeric">True</property> <property name="value">5</property> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">3</property> </packing> </child> <child> <object class="GtkButton" id="apply_config_button"> <property name="label" translatable="yes">Apply</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> <property name="image">apply_image</property> <property name="always-show-image">True</property> <accelerator key="s" signal="clicked" modifiers="GDK_CONTROL_MASK"/> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">4</property> </packing> </child> <child> <placeholder/> </child> </object> <packing> <property name="name">settings</property> <property name="title" translatable="yes">Settings</property> <property name="icon-name">applications-system-symbolic</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkBox" id="info_box"> <property name="height-request">28</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="margin-start">5</property> <property name="margin-end">5</property> <property name="margin-bottom">5</property> <property name="spacing">5</property> <child> <object class="GtkImage" id="info_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">dialog-information-symbolic</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkLabel" id="info_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">OScam version:</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkLabel" id="version_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label">0</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <object class="GtkBox" id="readers_count_box"> <property name="width-request">50</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="spacing">5</property> <child> <object class="GtkImage" id="readers_count_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">document-properties</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkLabel" id="readers_count_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label">0</property> <property name="ellipsize">end</property> <property name="max-width-chars">5</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="pack-type">end</property> <property name="position">3</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> </object> </child> </object> </interface> 07070100000017000041ED00000000000000000000000266D18CCB00000000000000000000000000000000000000000000002F00000000demoneditor-extensions/extensions/streamimport07070100000018000081A400000000000000000000000166D18CCB000000A6000000000000000000000000000000000000003900000000demoneditor-extensions/extensions/streamimport/README.md## Advanced streams import Allows you to import IPTV streams from various sources. ### Requirements [DemonEditor](https://github.com/DYefremov/DemonEditor) >= 3.4.207070100000019000081A400000000000000000000000166D18CCB000035D8000000000000000000000000000000000000003B00000000demoneditor-extensions/extensions/streamimport/__init__.py# -*- coding: utf-8 -*- # # The MIT License (MIT) # # Copyright (c) 2023 Dmitriy Yefremov # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # Author: Dmitriy Yefremov # import os import re from enum import IntEnum from itertools import groupby from gi.repository import Gtk, GLib from app.eparser import Service from app.eparser.ecommons import BqServiceType from app.eparser.iptv import MARKER_FORMAT, get_picon_id, get_fav_id from app.settings import SettingsType from app.ui.main_helper import update_toggle_model, update_popup_filter_model, get_base_itrs, scroll_to, on_popup_menu from app.ui.uicommons import IPTV_ICON, Column from extensions import BaseExtension class Streamimport(BaseExtension): LABEL = "Advanced streams import" VERSION = "1.1" def __init__(self, app): super().__init__(app) self._dialog = ImportDialog(self) def exec(self): self._dialog.show() class ImportDialog(Gtk.Window): PARAMS = re.compile(r'(\S+)="(.*?)"') class Column(IntEnum): LOGO = 0 NAME = 1 GROUP = 2 SELECTED = 3 ID = 4 URL = 5 LOGO_URL = 6 TOOLTIP = 7 def __init__(self, plugin, **kwargs): super().__init__(title=Streamimport.LABEL, destroy_with_parent=True, window_position=Gtk.WindowPosition.CENTER_ON_PARENT, transient_for=plugin.app.app_window, default_width=560, icon_name="demon-editor", **kwargs) self._plugin = plugin self._app = plugin.app self._groups = set() _base_path = os.path.dirname(__file__) builder = Gtk.Builder.new_from_file(f"{_base_path}{os.sep}dialog.ui") self.add(builder.get_object("main_box")) self._view = builder.get_object("view") self._popup_menu = builder.get_object("popup_menu") self._select_all_item = builder.get_object("select_all_item") self._remove_selection_item = builder.get_object("remove_selection_item") self._view.connect("select-all", lambda v: self.update_selection(True)) self._view.connect_data("button-press-event", self.on_button_press) self._select_all_item.connect("activate", lambda i: self.update_selection(True)) self._remove_selection_item.connect("activate", lambda i: self.update_selection(False)) self._model = builder.get_object("model") self._filter_model = builder.get_object("filter_model") self._filter_model.set_visible_func(self.filter_function) self._filter_group_model = builder.get_object("filter_group_list_store") self._filter_entry = builder.get_object("filter_entry") self._filter_entry.connect("search-changed", self.on_filter_changed) renderer_toggle = builder.get_object("filter_group_renderer_toggle") renderer_toggle.connect("toggled", self.on_group_toggled) self._chooser_button = builder.get_object("file_chooser_button") self._chooser_button.connect("file-set", self.on_file_set) self._input_text_view = builder.get_object("input_text_view") self._input_text_view.get_buffer().connect("paste-done", self.on_paste_text) builder.get_object("selected_renderer").connect("toggled", self.on_selected_toggled) builder.get_object("version_label").set_text(f"Ver: {Streamimport.VERSION}") self._service_type_box = builder.get_object("service_type_box") self._single_bq_button = builder.get_object("single_bq_button") self._split_bq_button = builder.get_object("split_bq_button") self._sub_bq_button = builder.get_object("sub_bq_button") builder.get_object("import_button").connect("clicked", self.on_import) self.connect("hide", self.clear_data) self.connect("delete-event", self.on_destroy) # Neutrino. builder.get_object("options_grid").set_visible(self._app.is_enigma) def on_destroy(self, window, event): """ Used to prevent window deletion and destroying. """ window.hide() return True def clear_data(self, widget=None): self._model.clear() self._groups.clear() self.update_groups() self._filter_entry.set_text("") def on_file_set(self, button): self.clear_data() path = button.get_filename() if not os.path.isfile(path): return gen = self.update_from_file(path) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def on_paste_text(self, buffer, clip): self.clear_data() text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) gen = self.process_data(text.splitlines()) GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW) def update_from_file(self, path): self._chooser_button.set_sensitive(False) with open(path, "rb") as file: data = file.read() encoding = "utf-8" try: import chardet except ModuleNotFoundError: pass else: enc = chardet.detect(data) encoding = enc.get("encoding", "utf-8") yield from self.process_data(str(data, encoding=encoding, errors="ignore").splitlines()) yield self._chooser_button.set_sensitive(True) def process_data(self, lines): group = None name = None logo = None ch_id = None for line in lines: if line.startswith("#EXTM3U"): params = dict(self.PARAMS.findall(line)) epg_src = params.get("x-tvg-url", params.get("url-tvg", None)) epg_src = epg_src.split(",") if epg_src else None elif line.startswith("#EXTINF"): line, sep, name = line.rpartition(",") params = dict(self.PARAMS.findall(line)) group = params.get("group-title", None) name = params.get("tvg-name", name) logo = params.get("tvg-logo", None) ch_id = params.get("tvg-id", None) elif line.startswith("#EXTGRP"): group = line.strip("#EXTGRP:").strip() elif not line.startswith("#") and "://" in line: url = line.strip() if name: name = name.strip() else: pass ch_id = ch_id.strip() if ch_id else ch_id logo = logo.strip() if logo else logo group = group or "No Group" self._groups.add(group) yield self._model.append((None, name, group, True, ch_id, url, logo, None)) self.update_groups() yield True def on_button_press(self, view, event): empty = bool(len(view.get_model())) self._select_all_item.set_sensitive(empty) self._remove_selection_item.set_sensitive(empty) on_popup_menu(self._popup_menu, event) def update_selection(self, select): model = self._view.get_model() iters = get_base_itrs((r.iter for r in model), model) [self._model.set_value(itr, self.Column.SELECTED, select) for itr in iters] def on_selected_toggled(self, renderer, path): model = self._view.get_model() itr = get_base_itrs((model.get_iter(path),), model).pop() self._model.set_value(itr, self.Column.SELECTED, not renderer.get_active()) def on_group_toggled(self, renderer, path): update_toggle_model(self._filter_group_model, path, renderer) self._groups.clear() self._groups.update({r[0] for r in self._filter_group_model if r[1]}) self.on_filter_changed() def update_groups(self): update_popup_filter_model(self._filter_group_model, self._groups) list(map(lambda g: self._filter_group_model.append((g, True)), sorted(self._groups, reverse=True))) def on_filter_changed(self, entry=None): self._filter_model.refilter() def filter_function(self, model, itr, data): if any((model is None, model == "None")): return True txt, grp = model[itr][self.Column.NAME], model[itr][self.Column.GROUP] txt = txt.upper() if txt else "" return self._filter_entry.get_text().upper() in txt and grp in self._groups def on_import(self, button): settings_type = self._app.app_settings.setting_type service_rows = filter(lambda r: r[self.Column.SELECTED], self._view.get_model()) if settings_type is SettingsType.ENIGMA_2: if not self._single_bq_button.get_active(): def grouper(row): return row[self.Column.GROUP] service_rows = groupby(sorted(service_rows, key=grouper), key=grouper) model = self._app.bouquets_view.get_model() if settings_type is SettingsType.ENIGMA_2: itr = model.get_iter_first() else: itr = model.get_iter(Gtk.TreePath.new_from_indices([len(model) - 1])) if not itr: msg = "Error. Load your data first!" self._plugin.log(msg) self._app.show_error_message(msg) return root_path = model.get_path(itr) bq_itr = itr if self._single_bq_button.get_active() or settings_type is SettingsType.NEUTRINO_MP: bq_itr = self.append_bouquet("IPTV", model, itr, service_rows, settings_type) elif self._split_bq_button.get_active(): for g, g_services in service_rows: bq_itr = self.append_bouquet(g, model, itr, g_services, settings_type) else: itr = self.append_bouquet("IPTV", model, itr, (), settings_type) bq_itr = itr for g, g_services in service_rows: self.append_bouquet(g, model, itr, g_services, settings_type) scroll_to(model.get_path(bq_itr), self._app.bouquets_view, [root_path]) self._app.show_info_message("Done!") def append_bouquet(self, name, model, itr, service_rows, settings_type): """ Adds new bouquet and returns iter of appended row. """ bqs = self._app.current_bouquets cur_services = self._app.current_services bq_type = model.get_value(itr, Column.BQ_TYPE) bq_name = self.get_bouquet_name(bqs, name, bq_type) services = self.get_group_services(service_rows, settings_type) bqs[f"{bq_name}:{bq_type}"] = [s.fav_id for s in services] cur_services.update({s.fav_id: s for s in services}) bq = (bq_name, None, None, bq_type) return model.append(itr, bq) def get_group_services(self, rows, settings_type): params = [0, 0, 0, 0] aggr = [None] * 10 s_aggr = aggr[: -3] m_name = BqServiceType.MARKER.name st = BqServiceType.IPTV.name p_id = "1_0_1_0_0_0_0_0_0_0.png" picon = None srv_type = None if settings_type is SettingsType.ENIGMA_2: srv_type = self._service_type_box.get_active_id() grp_services = [] groups = set() m_counter = 0 sid_counter = 0 for rs in rows: if settings_type is SettingsType.ENIGMA_2: grp = rs[self.Column.GROUP] if grp and grp not in groups: groups.add(grp) m_counter += 1 fav_id = MARKER_FORMAT.format(m_counter, grp, grp) grp_services.append(Service(None, None, None, grp, *aggr[0:3], m_name, *aggr, fav_id, None)) sid_counter += 1 params[0] = sid_counter name, url = rs[self.Column.NAME], rs[self.Column.URL] fav_id = get_fav_id(url, name, settings_type, params, srv_type) if settings_type is SettingsType.ENIGMA_2: p_id = get_picon_id(params, srv_type) if all((name, url, fav_id)): srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None) grp_services.append(srv) else: self._plugin.log(f"Import error: name[{name}], url[{url}], fav id[{fav_id}]") return grp_services def get_bouquet_name(self, bouquets, base_name, bq_type): count = 0 key = f"{base_name}:{bq_type}" bq_name = base_name # Generating name of new bouquet. while key in bouquets: count += 1 bq_name = f"{base_name}{count}" key = f"{bq_name}:{bq_type}" return bq_name if __name__ == "__main__": pass 0707010000001A000081A400000000000000000000000166D18CCB0000607A000000000000000000000000000000000000003900000000demoneditor-extensions/extensions/streamimport/dialog.ui<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.38.2 The MIT License (MIT) Copyright (c) Dmitriy Yefremov 2023 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Author: Dmitriy Yefremov --> <interface domain="demon-editor"> <requires lib="gtk+" version="3.22"/> <!-- interface-license-type mit --> <!-- interface-name Advanced streams import --> <!-- interface-description Extension for DemonEditor --> <!-- interface-copyright Dmitriy Yefremov 2023 --> <!-- interface-authors Dmitriy Yefremov --> <object class="GtkFileFilter" id="file_filter"> <mime-types> <mime-type>application/x-mpegURL</mime-type> <mime-type>vnd.apple.mpegURL</mime-type> </mime-types> <patterns> <pattern>*.m3u*</pattern> </patterns> </object> <object class="GtkListStore" id="filter_group_list_store"> <columns> <!-- column-name group --> <column type="gchararray"/> <!-- column-name selected --> <column type="gboolean"/> </columns> <data> <row> <col id="0" translatable="yes">All groups</col> <col id="1">True</col> </row> </data> </object> <object class="GtkPopover" id="groups_popover"> <property name="can-focus">False</property> <child> <object class="GtkScrolledWindow" id="groups_scrolled_window"> <property name="width-request">135</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="border-width">5</property> <property name="hscrollbar-policy">never</property> <property name="max-content-height">350</property> <property name="propagate-natural-height">True</property> <child> <object class="GtkTreeView" id="groups_view"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="model">filter_group_list_store</property> <property name="headers-visible">False</property> <property name="enable-search">False</property> <property name="search-column">0</property> <child internal-child="selection"> <object class="GtkTreeSelection"/> </child> <child> <object class="GtkTreeViewColumn" id="fiter_groups_column"> <property name="title" translatable="yes">Satellite</property> <child> <object class="GtkCellRendererText" id="filter_group_renderer_text"/> <attributes> <attribute name="text">0</attribute> </attributes> </child> <child> <object class="GtkCellRendererToggle" id="filter_group_renderer_toggle"> <property name="xalign">0.9800000190734863</property> </object> <attributes> <attribute name="active">1</attribute> </attributes> </child> </object> </child> </object> </child> </object> </child> </object> <object class="GtkImage" id="import_button_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">insert-link-symbolic</property> </object> <object class="GtkListStore" id="model"> <columns> <!-- column-name logo --> <column type="GdkPixbuf"/> <!-- column-name name --> <column type="gchararray"/> <!-- column-name group --> <column type="gchararray"/> <!-- column-name selected --> <column type="gboolean"/> <!-- column-name id --> <column type="gchararray"/> <!-- column-name url --> <column type="gchararray"/> <!-- column-name logo_url --> <column type="gchararray"/> <!-- column-name tooltip --> <column type="gchararray"/> </columns> </object> <object class="GtkTreeModelFilter" id="filter_model"> <property name="child-model">model</property> </object> <object class="GtkTreeModelSort" id="sort_model"> <property name="model">filter_model</property> </object> <object class="GtkImage" id="remove_selection_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">edit-undo</property> </object> <object class="GtkMenu" id="popup_menu"> <property name="visible">True</property> <property name="can-focus">False</property> <child> <object class="GtkImageMenuItem" id="select_all_item"> <property name="label">gtk-select-all</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="use-underline">True</property> <property name="use-stock">True</property> </object> </child> <child> <object class="GtkImageMenuItem" id="remove_selection_item"> <property name="label" translatable="yes">Remove selection</property> <property name="visible">True</property> <property name="can-focus">False</property> <property name="image">remove_selection_image</property> <property name="use-stock">False</property> </object> </child> </object> <object class="GtkTextBuffer" id="text_buffer"/> <object class="GtkBox" id="main_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="GtkBox" id="header_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="spacing">5</property> <child type="center"> <object class="GtkButtonBox" id="filter_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="tooltip-text" translatable="yes">Filter</property> <property name="halign">center</property> <property name="margin-end">5</property> <property name="margin-top">5</property> <property name="margin-bottom">5</property> <property name="layout-style">expand</property> <child> <object class="GtkSearchEntry" id="filter_entry"> <property name="visible">True</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> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> <property name="non-homogeneous">True</property> </packing> </child> <child> <object class="GtkMenuButton" id="group_filter_button"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="focus-on-click">False</property> <property name="receives-default">True</property> <property name="popover">groups_popover</property> <child> <object class="GtkLabel"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="label" translatable="yes">Group</property> </object> </child> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="pack-type">end</property> <property name="position">1</property> <property name="secondary">True</property> <property name="non-homogeneous">True</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkFileChooserButton" id="file_chooser_button"> <property name="visible">True</property> <property name="sensitive" bind-source="text_input_button" bind-property="active" bind-flags="invert-boolean">True</property> <property name="can-focus">False</property> <property name="tooltip-text" translatable="yes">Import m3u file</property> <property name="margin-start">5</property> <property name="margin-top">5</property> <property name="margin-bottom">5</property> <property name="create-folders">False</property> <property name="filter">file_filter</property> <property name="local-only">False</property> <property name="title" translatable="yes">Select *.m3u*</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkToggleButton" id="text_input_button"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> <property name="tooltip-text" translatable="yes">Import from clipboard</property> <property name="margin-top">5</property> <property name="margin-bottom">5</property> <child> <object class="GtkImage" id="text_input_button_image"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="icon-name">edit-paste-symbolic</property> </object> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <style> <class name="primary-toolbar"/> </style> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkPaned"> <property name="height-request">200</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="margin-start">5</property> <property name="margin-end">5</property> <property name="orientation">vertical</property> <property name="wide-handle">True</property> <child> <object class="GtkScrolledWindow" id="input_text_view_scrolled"> <property name="visible" bind-source="text_input_button" bind-property="active">False</property> <property name="can-focus">True</property> <property name="shadow-type">in</property> <child> <object class="GtkTextView" id="input_text_view"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="tooltip-text" translatable="yes">Insert part or full playlist content!</property> <property name="wrap-mode">word</property> <property name="left-margin">5</property> <property name="right-margin">5</property> <property name="top-margin">5</property> <property name="bottom-margin">5</property> <property name="indent">5</property> <property name="buffer">text_buffer</property> </object> </child> </object> <packing> <property name="resize">True</property> <property name="shrink">False</property> </packing> </child> <child> <object class="GtkScrolledWindow" id="view_scrolled"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="shadow-type">in</property> <child> <object class="GtkTreeView" id="view"> <property name="visible">True</property> <property name="can-focus">True</property> <property name="model">sort_model</property> <property name="search-column">1</property> <child internal-child="selection"> <object class="GtkTreeSelection"/> </child> <child> <object class="GtkTreeViewColumn" id="name_column"> <property name="resizable">True</property> <property name="min-width">100</property> <property name="title" translatable="yes">Name</property> <property name="expand">True</property> <property name="alignment">0.5</property> <property name="sort-column-id">1</property> <child> <object class="GtkCellRendererPixbuf" id="logo_renderer"/> <attributes> <attribute name="pixbuf">0</attribute> </attributes> </child> <child> <object class="GtkCellRendererText" id="name_renderer"/> <attributes> <attribute name="text">1</attribute> </attributes> </child> </object> </child> <child> <object class="GtkTreeViewColumn" id="group_column"> <property name="resizable">True</property> <property name="min-width">100</property> <property name="title" translatable="yes">Group</property> <property name="expand">True</property> <property name="alignment">0.5</property> <property name="sort-column-id">2</property> <child> <object class="GtkCellRendererText" id="group_renderer"> <property name="xalign">0.49000000953674316</property> </object> <attributes> <attribute name="text">2</attribute> </attributes> </child> </object> </child> <child> <object class="GtkTreeViewColumn" id="selected_column"> <property name="sizing">fixed</property> <property name="fixed-width">120</property> <property name="title" translatable="yes">Selected</property> <property name="alignment">0.5</property> <property name="sort-column-id">3</property> <child> <object class="GtkCellRendererToggle" id="selected_renderer"/> <attributes> <attribute name="active">3</attribute> </attributes> </child> </object> </child> </object> </child> </object> <packing> <property name="resize">True</property> <property name="shrink">False</property> </packing> </child> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <!-- n-columns=2 n-rows=2 --> <object class="GtkGrid" id="options_grid"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="margin-start">15</property> <property name="margin-end">15</property> <property name="margin-top">5</property> <property name="margin-bottom">5</property> <property name="row-spacing">5</property> <property name="column-spacing">5</property> <property name="column-homogeneous">True</property> <child> <object class="GtkLabel"> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">Download logos</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">0</property> </packing> </child> <child> <object class="GtkSwitch" id="download_logos_switch"> <property name="can-focus">True</property> <property name="halign">end</property> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">0</property> </packing> </child> <child> <object class="GtkLabel"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">start</property> <property name="label" translatable="yes">Service type</property> </object> <packing> <property name="left-attach">0</property> <property name="top-attach">1</property> </packing> </child> <child> <object class="GtkComboBoxText" id="service_type_box"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">end</property> <property name="active">1</property> <property name="active-id">non-TS</property> <items> <item id="1" translatable="yes">DVB/TS</item> <item id="4097" translatable="yes">non-TS</item> <item id="5001" translatable="yes">none-REC1</item> <item id="5002" translatable="yes">none-REC2</item> </items> </object> <packing> <property name="left-attach">1</property> <property name="top-attach">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">3</property> </packing> </child> <child> <object class="GtkButtonBox" id="bouquets_split_box"> <property name="visible" bind-source="options_grid" bind-property="visible">True</property> <property name="can-focus">False</property> <property name="tooltip-text" translatable="yes">Create bouquets</property> <property name="halign">center</property> <property name="margin-start">15</property> <property name="margin-end">15</property> <property name="homogeneous">True</property> <property name="layout-style">expand</property> <child> <object class="GtkRadioButton" id="single_bq_button"> <property name="label" translatable="yes">Single bouquet</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">False</property> <property name="draw-indicator">False</property> <property name="group">sub_bq_button</property> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkRadioButton" id="split_bq_button"> <property name="label" translatable="yes">Split by groups</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">False</property> <property name="draw-indicator">False</property> <property name="group">single_bq_button</property> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkRadioButton" id="sub_bq_button"> <property name="label" translatable="yes">Create sub-bouquets</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">False</property> <property name="draw-indicator">False</property> <property name="group">split_bq_button</property> </object> <packing> <property name="expand">True</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="position">4</property> </packing> </child> <child> <object class="GtkButton" id="import_button"> <property name="label" translatable="yes">Import</property> <property name="width-request">200</property> <property name="visible">True</property> <property name="can-focus">True</property> <property name="receives-default">True</property> <property name="halign">center</property> <property name="margin-top">10</property> <property name="image">import_button_image</property> <property name="always-show-image">True</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">5</property> </packing> </child> <child> <object class="GtkLabel" id="version_label"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="halign">end</property> <property name="margin-end">10</property> <property name="margin-bottom">5</property> <property name="label" translatable="yes">Version</property> <attributes> <attribute name="style" value="italic"/> <attribute name="size" value="8000"/> </attributes> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">6</property> </packing> </child> </object> </interface> 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!221 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor