From 70470272a2fdb4f8bf64d523fbd36048f130b219 Mon Sep 17 00:00:00 2001 From: Nate Date: Sat, 3 Oct 2020 01:19:57 -0400 Subject: [PATCH 01/18] Get filler list by name from dizquetv.py --- dizqueTV/dizquetv.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index 5ab5cd2..f35c4ac 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -419,6 +419,17 @@ def get_filler_list(self, filler_list_id: str) -> Union[FillerList, None]: return FillerList(data=filler_list_data, dizque_instance=self) return None + def get_filler_list_by_name(self, filler_list_name: str) -> Union[FillerList, None]: + """ + Get a specific dizqueTV filler list + :param filler_list_name: name of filler list + :return: FillerList object + """ + for filler_list in self.filler_lists: + if filler_list.name == filler_list_name: + return filler_list + return None + def get_filler_list_info(self, filler_list_id: str) -> json: """ Get the name, content and id for a dizqueTV filler list From eb4afb9d93b0f1433fd44fd967f3141b60b7ade9 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 28 Sep 2020 23:56:17 -0400 Subject: [PATCH 02/18] Python 3.8 used for workflow --- .github/workflows/pypi_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi_publish.yml b/.github/workflows/pypi_publish.yml index 0e57306..eda522a 100644 --- a/.github/workflows/pypi_publish.yml +++ b/.github/workflows/pypi_publish.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.8' - name: Install dependencies run: | python -m pip install --upgrade pip From 181f94c235b813016363c6b9688f6895a4e0b5ee Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 4 Oct 2020 21:54:14 -0400 Subject: [PATCH 03/18] Redirect can be in add_channel --- dizqueTV/dizquetv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index f35c4ac..0983587 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -14,7 +14,7 @@ from dizqueTV.channels import Channel from dizqueTV.guide import Guide from dizqueTV.fillers import FillerList -from dizqueTV.media import FillerItem, Program +from dizqueTV.media import FillerItem, Program, Redirect from dizqueTV.plex_server import PlexServer from dizqueTV.templates import PLEX_SERVER_SETTINGS_TEMPLATE, CHANNEL_SETTINGS_TEMPLATE, CHANNEL_SETTINGS_DEFAULT, \ FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_SETTINGS_DEFAULT @@ -339,7 +339,7 @@ def _fill_in_default_channel_settings(self, settings_dict: dict, handle_errors: return helpers._combine_settings(new_settings_dict=settings_dict, old_settings_dict=CHANNEL_SETTINGS_DEFAULT) def add_channel(self, - programs: List[Union[Program, Video, Movie, Episode]] = None, + programs: List[Union[Program, Redirect, Video, Movie, Episode]] = None, plex_server: PServer = None, handle_errors: bool = True, **kwargs) -> Union[Channel, None]: @@ -354,7 +354,7 @@ def add_channel(self, """ kwargs['programs'] = [] for item in programs: - if type(item) == Program: + if type(item) in [Program, Redirect]: kwargs['programs'].append(item._data) else: if not plex_server: From 715149a68b117f33c24f97b8bb94ee05dfcfdb83 Mon Sep 17 00:00:00 2001 From: Nate Date: Sun, 4 Oct 2020 22:16:56 -0400 Subject: [PATCH 04/18] Store plex_server in Channel object for reuse --- dizqueTV/channels.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index 72473d8..bd0730c 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -14,7 +14,7 @@ class Channel: - def __init__(self, data: json, dizque_instance): + def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self._data = data self._dizque_instance = dizque_instance self._program_data = data.get('programs') @@ -37,6 +37,7 @@ def __init__(self, data: json, dizque_instance): self.duration = data.get('duration') self.stealth = data.get('stealth') self._id = data.get('_id') + self.plex_server = plex_server def __repr__(self): return f"<{self.__class__.__name__}:{self.number}:{self.name}>" @@ -132,9 +133,11 @@ def add_program(self, """ if not plex_item and not program and not kwargs: raise MissingParametersError("Please include either a program, a plex_item/plex_server combo, or kwargs") - if plex_item and plex_server: + if plex_item and (plex_server or self.plex_server): temp_program = self._dizque_instance.convert_plex_item_to_program(plex_item=plex_item, - plex_server=plex_server) + plex_server=( + plex_server if plex_server else self.plex_server) + ) kwargs = temp_program._data elif program: kwargs = program._data @@ -166,10 +169,13 @@ def add_programs(self, programs: List[Union[Program, Video, Movie, Episode]], pl raise Exception("You must provide at least one program to add to the channel.") for program in programs: if type(program) not in [Program, Redirect]: - if not plex_server: + if not plex_server and not self.plex_server: raise MissingParametersError("Please include a plex_server if you are adding PlexAPI Video, " "Movie, or Episode items.") - program = self._dizque_instance.convert_plex_item_to_program(plex_item=program, plex_server=plex_server) + program = self._dizque_instance.convert_plex_item_to_program(plex_item=program, + plex_server=( + plex_server if plex_server else self.plex_server) + ) channel_data['programs'].append(program._data) channel_data['duration'] += program.duration return self.update(**channel_data) From 1e3f3ffab502ea06a97bbd6acd3653dd90b121ba Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 6 Oct 2020 10:46:37 -0400 Subject: [PATCH 05/18] Added custom __repr__ to all classes --- dizqueTV/dizquetv.py | 9 ++++++--- dizqueTV/guide.py | 9 +++++++++ dizqueTV/media.py | 12 ++++++------ dizqueTV/plex_server.py | 3 +++ dizqueTV/settings.py | 12 ++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index 0983587..fadb160 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -95,6 +95,9 @@ def __init__(self, url: str, verbose: bool = False): logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=(logging.INFO if verbose else logging.ERROR)) + def __repr__(self): + return f"<{self.__class__.__name__}:{self.url}>" + def _get(self, endpoint: str, params: dict = None, headers: dict = None, timeout: int = 2) -> Union[Response, None]: if not endpoint.startswith('/'): endpoint = f"/{endpoint}" @@ -785,9 +788,9 @@ def add_programs_to_channels(self, programs: List[Program], return True def add_filler_lists_to_channels(self, - filler_lists: List[FillerList], - channels: List[Channel] = None, - channel_numbers: List[int] = None) -> bool: + filler_lists: List[FillerList], + channels: List[Channel] = None, + channel_numbers: List[int] = None) -> bool: """ Add multiple filler lists to multiple channels :param filler_lists: List of FillerList objects diff --git a/dizqueTV/guide.py b/dizqueTV/guide.py index e997a0a..e0b9017 100644 --- a/dizqueTV/guide.py +++ b/dizqueTV/guide.py @@ -16,6 +16,9 @@ def __init__(self, data): self.icon = data.get('icon') self.title = data.get('title') + def __repr__(self): + return f"<{self.__class__.__name__}:{self.title}>" + class GuideChannel: def __init__(self, data, programs, dizque_instance): @@ -26,6 +29,9 @@ def __init__(self, data, programs, dizque_instance): self.number = data.get('number') self.programs = programs + def __repr__(self): + return f"<{self.__class__.__name__}:{self.name}>" + def get_lineup(self, from_date: datetime, to_date: datetime) -> List[GuideProgram]: """ Get guide channel lineup for a certain time range @@ -49,6 +55,9 @@ def __init__(self, data, dizque_instance): self._dizque_instance = dizque_instance self.channels = self._create_channels_and_programs() + def __repr__(self): + return f"<{self.__class__.__name__}>" + def _create_channels_and_programs(self): channels = [] for channel_number, data in self._data.items(): diff --git a/dizqueTV/media.py b/dizqueTV/media.py index cb0734b..beea469 100644 --- a/dizqueTV/media.py +++ b/dizqueTV/media.py @@ -72,6 +72,9 @@ def __init__(self, data: json, dizque_instance, channel_instance): super().__init__(data=data, dizque_instance=dizque_instance, channel_instance=channel_instance) self.rating = data.get('rating') + def __repr__(self): + return f"<{self.__class__.__name__}:{self.title}>" + @_check_for_dizque_instance def delete(self) -> bool: """ @@ -80,15 +83,15 @@ def delete(self) -> bool: """ return self._channel_instance.delete_program(program=self) - def __repr__(self): - return f"<{self.__class__.__name__}:{self.title}>" - class FillerItem(MediaItem): def __init__(self, data: json, dizque_instance, filler_list_instance): super().__init__(data=data, dizque_instance=dizque_instance) self._filler_list_instance = filler_list_instance + def __repr__(self): + return f"<{self.__class__.__name__}:{self.title}>" + @_check_for_dizque_instance def delete(self) -> bool: """ @@ -96,6 +99,3 @@ def delete(self) -> bool: :return: True if successful, False if unsuccessful """ return self._filler_list_instance.delete_filler(filler=self) - - def __repr__(self): - return f"<{self.__class__.__name__}:{self.title}>" diff --git a/dizqueTV/plex_server.py b/dizqueTV/plex_server.py index 76f512e..169708c 100644 --- a/dizqueTV/plex_server.py +++ b/dizqueTV/plex_server.py @@ -15,6 +15,9 @@ def __init__(self, data: json, dizque_instance): self.arGuide = data.get('arGuide') self._id = data.get('_id') + def __repr__(self): + return f"<{self.__class__.__name__}:{self.name}>" + @property @helpers._check_for_dizque_instance def status(self) -> bool: diff --git a/dizqueTV/settings.py b/dizqueTV/settings.py index ccc17db..fa18639 100644 --- a/dizqueTV/settings.py +++ b/dizqueTV/settings.py @@ -12,6 +12,9 @@ def __init__(self, data: json, dizque_instance): self.file = data.get('file') self._id = data.get('_id') + def __repr__(self): + return f"<{self.__class__.__name__}:{self._id}>" + @helpers._check_for_dizque_instance def reload(self): """ @@ -57,6 +60,9 @@ def __init__(self, data: json, dizque_instance): self.autoDiscovery = data.get('autoDiscovery') self._id = data.get('_id') + def __repr__(self): + return f"<{self.__class__.__name__}:{self._id}>" + @helpers._check_for_dizque_instance def refresh(self): """ @@ -123,6 +129,9 @@ def __init__(self, data: json, dizque_instance): self.maxFPS = data.get('maxFPS') self._id = data.get('_id') + def __repr__(self): + return f"<{self.__class__.__name__}:{self._id}>" + @helpers._check_for_dizque_instance def refresh(self): """ @@ -185,6 +194,9 @@ def __init__(self, data: json, dizque_instance): self.pathReplaceWith = data.get('pathReplaceWith') self._id = data.get('_id') + def __repr__(self): + return f"<{self.__class__.__name__}:{self._id}>" + @helpers._check_for_dizque_instance def refresh(self): """ From c3e7dd18a988431d58785865ea37586b58114d56 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 12 Oct 2020 10:46:54 -0400 Subject: [PATCH 06/18] Functions to add X number/duration of items to a channel --- dizqueTV/channels.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index bd0730c..7ccb67c 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -218,6 +218,72 @@ def delete_show(self, show_name: str, season_number: int = None) -> bool: return self.add_programs(programs=programs_to_add) return False + @helpers._check_for_dizque_instance + def add_x_number_of_show_episodes(self, + number_of_episodes: int, + list_of_episodes: List[Program, Episode], + plex_server: PServer = None) -> bool: + """ + Add the first X number of items from a list of programs to a dizqueTV channel + :param number_of_episodes: number of items to add from the list + :param list_of_episodes: list of Program or plexapi.media.Episode objects + :param plex_server: plexapi.server, needed if adding plexapi.media.Episode objects + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + channel_data = self._data + for i in range(0, number_of_episodes): + if not type(list_of_episodes[i]) == Program: + if not plex_server and not self.plex_server: + raise MissingParametersError("Please include a plex_server if you are adding PlexAPI Video " + "or Episode items.") + list_of_episodes[i] = self._dizque_instance.convert_plex_item_to_program(plex_item=list_of_episodes[i], + plex_server=( + plex_server if plex_server else self.plex_server) + ) + channel_data['programs'].append(list_of_episodes[i]._data) + channel_data['duration'] += list_of_episodes[i].duration + return self.update(**channel_data) + + @helpers._check_for_dizque_instance + def add_x_duration_of_show_episodes(self, + duration_in_milliseconds: int, + list_of_episodes: List[Program, Episode], + plex_server: PServer = None, + allow_overtime: bool = False) -> bool: + """ + Add an X duration of items from a list of programs to a dizqueTV channel + :param duration_in_milliseconds: length of time to add + :param list_of_episodes: list of Program or plexapi.media.Episode objects + :param plex_server: plexapi.server, needed if adding plexapi.media.Episode objects + :param allow_overtime: Allow adding one more episode, even if total time would go over. + Otherwise, don't add any more if total time would exceed duration_in_milliseconds (default: False) + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + channel_data = self._data + total_runtime = 0 + list_index = 0 + while total_runtime < duration_in_milliseconds: + if not type(list_of_episodes[list_index]) == Program: + if not plex_server and not self.plex_server: + raise MissingParametersError("Please include a plex_server if you are adding PlexAPI Video " + "or Episode items.") + list_of_episodes[list_index] = self._dizque_instance.convert_plex_item_to_program(plex_item=list_of_episodes[list_index], + plex_server=( + plex_server if plex_server else self.plex_server) + ) + if (total_runtime + list_of_episodes[list_index].duration) > duration_in_milliseconds: + if allow_overtime: + channel_data['programs'].append(list_of_episodes[list_index]._data) + channel_data['duration'] += list_of_episodes[list_index].duration + else: + pass + else: + channel_data['programs'].append(list_of_episodes[list_index]._data) + channel_data['duration'] += list_of_episodes[list_index].duration + total_runtime += list_of_episodes[list_index].duration + list_index += 1 + return self.update(**channel_data) + @helpers._check_for_dizque_instance def delete_all_programs(self) -> bool: """ From 0a3fb1138e6e4f878f9b987e9b1e5d059b1e2590 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 12 Oct 2020 14:15:11 -0400 Subject: [PATCH 07/18] New Watermark object to ingest and edit/update watermark on channel. Templates updated accordingly --- dizqueTV/channels.py | 91 +++++++++++++++++++++++++++++++++++-------- dizqueTV/dizquetv.py | 22 ++++++++++- dizqueTV/helpers.py | 17 +++++++- dizqueTV/templates.py | 38 +++++++++++++----- 4 files changed, 139 insertions(+), 29 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index 7ccb67c..bd641bc 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -13,6 +13,46 @@ from dizqueTV.exceptions import MissingParametersError +class Watermark: + def __init__(self, data: dict, dizque_instance, channel_instance): + self._data = data + self._dizque_instance = dizque_instance + self._channel_instance = channel_instance + self.enabled = data.get('enabled') + self.width = data.get('width') + self.verticalMargin = data.get('verticalMargin') + self.horizontalMargin = data.get('horizontalMargin') + self.duration = data.get('duration') + self.fixedSize = data.get('fixedSize') + self.position = data.get('position') + self.url = data.get('url') + self.animated = data.get('animated') + + @property + def json(self): + """ + Get watermark JSON + :return: JSON data for watermark object + """ + return self._data + + @helpers._check_for_dizque_instance + def update(self, **kwargs) -> bool: + """ + Edit this Watermark on dizqueTV + Automatically refreshes associated Channel object + :param kwargs: keyword arguments of Watermark settings names and values + :return: True if successful, False if unsuccessful (Channel reloads in-place, Watermark object is destroyed) + """ + new_watermark_dict = self._dizque_instance._fill_in_watermark_settings(**kwargs) + if self._dizque_instance.update_channel(channel_number=self._channel_instance.number, + watermark=new_watermark_dict): + self._channel_instance.refresh() + del self + return True + return False + + class Channel: def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self._data = data @@ -20,14 +60,6 @@ def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self._program_data = data.get('programs') self._fillerCollections_data = data.get('fillerCollections') self.fillerRepeatCooldown = data.get('fillerRepeatCooldown') - self.fallback = [FillerItem(data=filler_data, dizque_instance=dizque_instance, filler_list_instance=None) - for filler_data in data.get('fallback')] - self.icon = data.get('icon') - self.disableFillerOverlay = data.get('disableFillerOverlay') - self.iconWidth = data.get('iconWidth') - self.iconDuration = data.get('iconDuration') - self.iconPosition = data.get('iconPosition') - self.overlayIcon = data.get('overlayIcon') self.startTime = data.get('startTime') self.offlinePicture = data.get('offlinePicture') self.offlineSoundtrack = data.get('offlineSoundtrack') @@ -37,6 +69,9 @@ def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self.duration = data.get('duration') self.stealth = data.get('stealth') self._id = data.get('_id') + self.fallback = [FillerItem(data=filler_data, dizque_instance=dizque_instance, filler_list_instance=None) + for filler_data in data.get('fallback')] + self.watermark = Watermark(data=data.get('watermark'), dizque_instance=dizque_instance, channel_instance=self) self.plex_server = plex_server def __repr__(self): @@ -91,6 +126,14 @@ def get_filler_list(self, filler_list_title: str) -> Union[FillerList, None]: return filler_list return None + @property + def json(self): + """ + Get channel JSON + :return: JSON data for channel object + """ + return self._data + # Update @helpers._check_for_dizque_instance def refresh(self): @@ -117,6 +160,15 @@ def update(self, **kwargs) -> bool: return True return False + @helpers._check_for_dizque_instance + def edit(self, **kwargs) -> bool: + """ + Alias for channels.update() + :param kwargs: keyword arguments of Channel settings names and values + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + return self.update(**kwargs) + @helpers._check_for_dizque_instance def add_program(self, plex_item: Union[Video, Movie, Episode] = None, @@ -136,7 +188,8 @@ def add_program(self, if plex_item and (plex_server or self.plex_server): temp_program = self._dizque_instance.convert_plex_item_to_program(plex_item=plex_item, plex_server=( - plex_server if plex_server else self.plex_server) + plex_server if plex_server + else self.plex_server) ) kwargs = temp_program._data elif program: @@ -174,7 +227,8 @@ def add_programs(self, programs: List[Union[Program, Video, Movie, Episode]], pl "Movie, or Episode items.") program = self._dizque_instance.convert_plex_item_to_program(plex_item=program, plex_server=( - plex_server if plex_server else self.plex_server) + plex_server if plex_server + else self.plex_server) ) channel_data['programs'].append(program._data) channel_data['duration'] += program.duration @@ -221,7 +275,7 @@ def delete_show(self, show_name: str, season_number: int = None) -> bool: @helpers._check_for_dizque_instance def add_x_number_of_show_episodes(self, number_of_episodes: int, - list_of_episodes: List[Program, Episode], + list_of_episodes: List[Union[Program, Episode]], plex_server: PServer = None) -> bool: """ Add the first X number of items from a list of programs to a dizqueTV channel @@ -238,7 +292,8 @@ def add_x_number_of_show_episodes(self, "or Episode items.") list_of_episodes[i] = self._dizque_instance.convert_plex_item_to_program(plex_item=list_of_episodes[i], plex_server=( - plex_server if plex_server else self.plex_server) + plex_server if plex_server + else self.plex_server) ) channel_data['programs'].append(list_of_episodes[i]._data) channel_data['duration'] += list_of_episodes[i].duration @@ -247,7 +302,7 @@ def add_x_number_of_show_episodes(self, @helpers._check_for_dizque_instance def add_x_duration_of_show_episodes(self, duration_in_milliseconds: int, - list_of_episodes: List[Program, Episode], + list_of_episodes: List[Union[Program, Episode]], plex_server: PServer = None, allow_overtime: bool = False) -> bool: """ @@ -267,10 +322,12 @@ def add_x_duration_of_show_episodes(self, if not plex_server and not self.plex_server: raise MissingParametersError("Please include a plex_server if you are adding PlexAPI Video " "or Episode items.") - list_of_episodes[list_index] = self._dizque_instance.convert_plex_item_to_program(plex_item=list_of_episodes[list_index], - plex_server=( - plex_server if plex_server else self.plex_server) - ) + list_of_episodes[list_index] = \ + self._dizque_instance.convert_plex_item_to_program(plex_item=list_of_episodes[list_index], + plex_server=( + plex_server if plex_server + else self.plex_server) + ) if (total_runtime + list_of_episodes[list_index].duration) > duration_in_milliseconds: if allow_overtime: channel_data['programs'].append(list_of_episodes[list_index]._data) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index fadb160..badb998 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -17,7 +17,7 @@ from dizqueTV.media import FillerItem, Program, Redirect from dizqueTV.plex_server import PlexServer from dizqueTV.templates import PLEX_SERVER_SETTINGS_TEMPLATE, CHANNEL_SETTINGS_TEMPLATE, CHANNEL_SETTINGS_DEFAULT, \ - FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_SETTINGS_DEFAULT + FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_SETTINGS_DEFAULT, WATERMARK_SETTINGS_DEFAULT import dizqueTV.helpers as helpers from dizqueTV.exceptions import MissingParametersError, ChannelCreationError, ItemCreationError @@ -308,6 +308,25 @@ def channel_numbers(self) -> List[int]: return data return [] + def _fill_in_watermark_settings(self, handle_errors: bool = True, **kwargs) -> dict: + """ + Create complete watermark settings + :param kwargs: All kwargs, including some related to watermark + :return: A complete and valid watermark dict + """ + final_dict = helpers._combine_settings_by_template(new_settings_dict=kwargs, + settings_template=WATERMARK_SETTINGS_DEFAULT) + if handle_errors and final_dict['enabled'] is True: + if not (0 < final_dict['width'] <= 100): + raise Exception("Watermark width must greater than 0 and less than 100") + if not (final_dict['width'] + final_dict['horizontalMargin'] <= 100): + raise Exception("Watermark width + horizontalMargin must not be greater than 100") + if not (final_dict['verticalMargin'] <= 100): + raise Exception("Watermark verticalMargin must not be greater than 100") + if not (final_dict['duration'] and final_dict['duration'] >= 0): + raise Exception("Must include a watermark duration. Use 0 for a permanent watermark.") + return final_dict + def _fill_in_default_channel_settings(self, settings_dict: dict, handle_errors: bool = False) -> dict: """ Set some dynamic default values, such as channel number, start time and image URLs @@ -339,6 +358,7 @@ def _fill_in_default_channel_settings(self, settings_dict: dict, handle_errors: settings_dict['offlinePicture'] = f"{self.url}/images/generic-offline-screen.png" # override duration regardless of user input settings_dict['duration'] = sum(program['duration'] for program in settings_dict['programs']) + settings_dict['watermark'] = self._fill_in_watermark_settings(**settings_dict) return helpers._combine_settings(new_settings_dict=settings_dict, old_settings_dict=CHANNEL_SETTINGS_DEFAULT) def add_channel(self, diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index f5cf7e7..eff5501 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -32,9 +32,10 @@ def inner(obj, **kwargs): # Internal Helpers -def _combine_settings(new_settings_dict: json, old_settings_dict: json) -> json: +def _combine_settings(new_settings_dict: dict, old_settings_dict: dict) -> dict: """ Build a complete dictionary for new settings, using old settings as a base + Add new keys to template. :param new_settings_dict: Dictionary of new settings kwargs :param old_settings_dict: Current settings :return: Dictionary of new settings @@ -44,6 +45,20 @@ def _combine_settings(new_settings_dict: json, old_settings_dict: json) -> json: return old_settings_dict +def _combine_settings_by_template(new_settings_dict: dict, settings_template: dict) -> dict: + """ + Build a complete dictionary for new settings, using old settings as a base + Do not add new keys to template. + :param new_settings_dict: Dictionary of new settings kwargs + :param settings_template: settings template + :return: Dictionary of new settings + """ + for k, v in new_settings_dict.items(): + if k in settings_template.keys(): + settings_template[k] = v + return settings_template + + def _settings_are_complete(new_settings_dict: json, template_settings_dict: json, ignore_id: bool = False) -> bool: """ Check that all elements from the settings template are present in the new settings diff --git a/dizqueTV/templates.py b/dizqueTV/templates.py index 668c7ba..01c828c 100644 --- a/dizqueTV/templates.py +++ b/dizqueTV/templates.py @@ -10,16 +10,37 @@ "_id": str } +WATERMARK_SETTINGS_TEMPLATE = { + "enabled": bool, + "width": float, + "verticalMargin": float, + "horizontalMargin": float, + "duration": int, + "fixedSize": bool, + "position": str, + "url": str, + "animated": bool +} + +WATERMARK_SETTINGS_DEFAULT = { + "enabled": False, + "width": 6.25, + "verticalMargin": 1.8518518518518519, + "horizontalMargin": 1.0416666666666667, + "duration": 60, + "fixedSize": False, + "position": "bottom-right", + "url": "", + "animated": False +} + CHANNEL_SETTINGS_TEMPLATE = { "programs": List, - "fillerContent": List, + "fillerCollections": List, "fillerRepeatCooldown": int, "fallback": [], "icon": str, "disableFillerOverlay": bool, - "iconWidth": int, - "iconDuration": int, - "iconPosition": str, "startTime": str, "offlinePicture": str, "offlineSoundtrack": str, @@ -28,21 +49,18 @@ "name": str, "duration": int, "_id": str, - "overlayIcon": bool, + "watermark": {}, "stealth": bool } CHANNEL_SETTINGS_DEFAULT = { - "fillerContent": [], + "fillerCollections": [], "fillerRepeatCooldown": 1800000, "fallback": [], "disableFillerOverlay": True, - "iconWidth": 120, - "iconDuration": 60, - "iconPosition": "2", "offlineSoundtrack": "", "offlineMode": "pic", - "overlayIcon": False, + "watermark": WATERMARK_SETTINGS_DEFAULT, "stealth": False } From 0b42579cc0c8b81478e3ebd82325af1a20f60a19 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 15 Oct 2020 23:11:16 -0400 Subject: [PATCH 08/18] New edit_ffmpeg_settings function for Channel object --- dizqueTV/channels.py | 18 +++++++++++++++++- dizqueTV/templates.py | 26 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index bd641bc..45b3533 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -9,7 +9,8 @@ from dizqueTV.fillers import FillerList from dizqueTV.media import Redirect, Program, FillerItem from dizqueTV.templates import MOVIE_PROGRAM_TEMPLATE, EPISODE_PROGRAM_TEMPLATE, \ - REDIRECT_PROGRAM_TEMPLATE, FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_CHANNEL_TEMPLATE + REDIRECT_PROGRAM_TEMPLATE, FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_CHANNEL_TEMPLATE, \ + CHANNEL_FFMPEG_SETTINGS_DEFAULT from dizqueTV.exceptions import MissingParametersError @@ -169,6 +170,21 @@ def edit(self, **kwargs) -> bool: """ return self.update(**kwargs) + @helpers._check_for_dizque_instance + def edit_ffmpeg_settings(self, use_global_settings: bool = False, **kwargs) -> bool: + """ + Edit the FFMPEG settings for a this Channel + :param use_global_settings: Use global dizqueTV FFMPEG settings (default: False) + :param kwargs: keyword arguments of Channel FFMPEG settings names and values + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + if use_global_settings: + new_settings = CHANNEL_FFMPEG_SETTINGS_DEFAULT + else: + new_settings = helpers._combine_settings(new_settings_dict=kwargs, + old_settings_dict=CHANNEL_FFMPEG_SETTINGS_DEFAULT) + return self.update(transcoding=new_settings) + @helpers._check_for_dizque_instance def add_program(self, plex_item: Union[Video, Movie, Episode] = None, diff --git a/dizqueTV/templates.py b/dizqueTV/templates.py index 01c828c..8940c7e 100644 --- a/dizqueTV/templates.py +++ b/dizqueTV/templates.py @@ -34,9 +34,20 @@ "animated": False } +CHANNEL_FFMPEG_SETTINGS_TEMPLATE = { + "targetResolution": str, + "videoBitrate": int, + "videoBufSize": int +} + +CHANNEL_FFMPEG_SETTINGS_DEFAULT = { + "targetResolution": "", + "videoBitrate": None, + "videoBufSize": None +} + CHANNEL_SETTINGS_TEMPLATE = { "programs": List, - "fillerCollections": List, "fillerRepeatCooldown": int, "fallback": [], "icon": str, @@ -49,18 +60,27 @@ "name": str, "duration": int, "_id": str, + "fillerCollections": List, "watermark": {}, - "stealth": bool + "transcoding": {}, + "guideMinimumDurationSeconds": int, + "guideFlexPlaceholder": str, + "stealth": bool, + "enabled": bool } CHANNEL_SETTINGS_DEFAULT = { - "fillerCollections": [], "fillerRepeatCooldown": 1800000, "fallback": [], "disableFillerOverlay": True, "offlineSoundtrack": "", "offlineMode": "pic", + "fillerCollections": [], "watermark": WATERMARK_SETTINGS_DEFAULT, + "transcoding": CHANNEL_FFMPEG_SETTINGS_DEFAULT, + "guideMinimumDurationSeconds": 300, + "guideFlexPlaceholder": "", + "enabled": True, "stealth": False } From 94d5559335fe9b2b2fac60aab4a3dc437d7eb590 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 15 Oct 2020 23:34:46 -0400 Subject: [PATCH 09/18] Strike that, new ChannelFFMPEGSettings object with update function, just like Watermark object --- dizqueTV/channels.py | 72 +++++++++++++++++++++++++++++++++----------- dizqueTV/dizquetv.py | 22 +++++++------- 2 files changed, 66 insertions(+), 28 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index 45b3533..29ef100 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -14,6 +14,50 @@ from dizqueTV.exceptions import MissingParametersError +class ChannelFFMPEGSettings: + def __init__(self, data: dict, dizque_instance, channel_instance): + self._data = data + self._dizque_instance = dizque_instance + self._channel_instance = channel_instance + self.targetResolution = data.get('targetResolution') + self.videoBitrate = data.get('videoBitrate') + self.videoBufSize = data.get('videoBufSize') + + def __repr__(self): + return f"<{self.__class__.__name__}:{(self.targetResolution if self.targetResolution else 'Default')}>" + + @property + def json(self): + """ + Get ChannelFFMPEGSettings JSON + :return: JSON data for ChannelFFMPEGSettings object + """ + return self._data + + + @helpers._check_for_dizque_instance + def update(self, use_global_settings: bool = False, **kwargs) -> bool: + """ + Edit this channel's FFMPEG settings on dizqueTV + Automatically refreshes associated Channel object + :param use_global_settings: Use global dizqueTV FFMPEG settings (default: False) + :param kwargs: keyword arguments of Channel FFMPEG settings names and values + :return: True if successful, False if unsuccessful + (Channel reloads in-place, ChannelFFMPEGSettings object is destroyed) + """ + if use_global_settings: + new_settings = CHANNEL_FFMPEG_SETTINGS_DEFAULT + else: + new_settings = helpers._combine_settings(new_settings_dict=kwargs, + template_dict=CHANNEL_FFMPEG_SETTINGS_DEFAULT) + if self._dizque_instance.update_channel(channel_number=self._channel_instance.number, + transcoding=new_settings): + self._channel_instance.refresh() + del self + return True + return False + + class Watermark: def __init__(self, data: dict, dizque_instance, channel_instance): self._data = data @@ -29,6 +73,9 @@ def __init__(self, data: dict, dizque_instance, channel_instance): self.url = data.get('url') self.animated = data.get('animated') + def __repr__(self): + return f"<{self.__class__.__name__}:{self.enabled}:{(self.url if self.url else 'Empty URL')}>" + @property def json(self): """ @@ -70,9 +117,15 @@ def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self.duration = data.get('duration') self.stealth = data.get('stealth') self._id = data.get('_id') - self.fallback = [FillerItem(data=filler_data, dizque_instance=dizque_instance, filler_list_instance=None) + self.fallback = [FillerItem(data=filler_data, + dizque_instance=dizque_instance, filler_list_instance=None) for filler_data in data.get('fallback')] - self.watermark = Watermark(data=data.get('watermark'), dizque_instance=dizque_instance, channel_instance=self) + self.watermark = Watermark(data=data.get('watermark'), + dizque_instance=dizque_instance, + channel_instance=self) + self.transcoding = ChannelFFMPEGSettings(data=data.get('transcoding'), + dizque_instance=dizque_instance, + channel_instance=self) self.plex_server = plex_server def __repr__(self): @@ -170,21 +223,6 @@ def edit(self, **kwargs) -> bool: """ return self.update(**kwargs) - @helpers._check_for_dizque_instance - def edit_ffmpeg_settings(self, use_global_settings: bool = False, **kwargs) -> bool: - """ - Edit the FFMPEG settings for a this Channel - :param use_global_settings: Use global dizqueTV FFMPEG settings (default: False) - :param kwargs: keyword arguments of Channel FFMPEG settings names and values - :return: True if successful, False if unsuccessful (Channel reloads in-place) - """ - if use_global_settings: - new_settings = CHANNEL_FFMPEG_SETTINGS_DEFAULT - else: - new_settings = helpers._combine_settings(new_settings_dict=kwargs, - old_settings_dict=CHANNEL_FFMPEG_SETTINGS_DEFAULT) - return self.update(transcoding=new_settings) - @helpers._check_for_dizque_instance def add_program(self, plex_item: Union[Video, Movie, Episode] = None, diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index badb998..e1abd26 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -253,7 +253,7 @@ def update_plex_server(self, server_name: str, **kwargs) -> bool: server = self.get_plex_server(server_name=server_name) if server: old_settings = server._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._post(endpoint='/plex-servers', data=new_settings): return True return False @@ -314,8 +314,8 @@ def _fill_in_watermark_settings(self, handle_errors: bool = True, **kwargs) -> d :param kwargs: All kwargs, including some related to watermark :return: A complete and valid watermark dict """ - final_dict = helpers._combine_settings_by_template(new_settings_dict=kwargs, - settings_template=WATERMARK_SETTINGS_DEFAULT) + final_dict = helpers._combine_settings_add_new(new_settings_dict=kwargs, + template_dict=WATERMARK_SETTINGS_DEFAULT) if handle_errors and final_dict['enabled'] is True: if not (0 < final_dict['width'] <= 100): raise Exception("Watermark width must greater than 0 and less than 100") @@ -359,7 +359,7 @@ def _fill_in_default_channel_settings(self, settings_dict: dict, handle_errors: # override duration regardless of user input settings_dict['duration'] = sum(program['duration'] for program in settings_dict['programs']) settings_dict['watermark'] = self._fill_in_watermark_settings(**settings_dict) - return helpers._combine_settings(new_settings_dict=settings_dict, old_settings_dict=CHANNEL_SETTINGS_DEFAULT) + return helpers._combine_settings(new_settings_dict=settings_dict, template_dict=CHANNEL_SETTINGS_DEFAULT) def add_channel(self, programs: List[Union[Program, Redirect, Video, Movie, Episode]] = None, @@ -407,7 +407,7 @@ def update_channel(self, channel_number: int, **kwargs) -> bool: old_settings = channel._data if kwargs.get('iconPosition'): kwargs['iconPosition'] = helpers.convert_icon_position(position_text=kwargs['iconPosition']) - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings_add_new(new_settings_dict=kwargs, template_dict=old_settings) if self._post(endpoint="/channel", data=new_settings): return True return False @@ -481,7 +481,7 @@ def _fill_in_default_filler_list_settings(self, settings_dict: dict, handle_erro raise ChannelCreationError("You must include at least one program when creating a filler list.") if 'name' not in settings_dict.keys(): settings_dict['name'] = f"New List {len(self.filler_lists) + 1}" - return helpers._combine_settings(new_settings_dict=settings_dict, old_settings_dict=CHANNEL_SETTINGS_DEFAULT) + return helpers._combine_settings(new_settings_dict=settings_dict, template_dict=CHANNEL_SETTINGS_DEFAULT) def add_filler_list(self, content: List[Union[Program, Video, Movie, Episode]], @@ -527,7 +527,7 @@ def update_filler_list(self, filler_list_id: str, **kwargs) -> bool: filler_list = self.get_filler_list(filler_list_id=filler_list_id) if filler_list: old_settings = filler_list._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._post(endpoint=f"/filler/{filler_list_id}", data=new_settings): return True return False @@ -560,7 +560,7 @@ def update_ffmpeg_settings(self, **kwargs) -> bool: :return: True if successful, False if unsuccessful """ old_settings = self.ffmpeg_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._put(endpoint='/ffmpeg-settings', data=new_settings): return True return False @@ -594,7 +594,7 @@ def update_plex_settings(self, **kwargs) -> bool: :return: True if successful, False if unsuccessful """ old_settings = self.plex_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._put(endpoint='/plex-settings', data=new_settings): return True return False @@ -637,7 +637,7 @@ def update_xmltv_settings(self, **kwargs) -> bool: :return: True if successful, False if unsuccessful """ old_settings = self.xmltv_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._put(endpoint='/xmltv-settings', data=new_settings): return True return False @@ -671,7 +671,7 @@ def update_hdhr_settings(self, **kwargs) -> bool: :return: True if successful, False if unsuccessful """ old_settings = self.hdhr_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, old_settings_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) if self._put(endpoint='/hdhr-settings', data=new_settings): return True return False From 41018d1fa7fb704255c264c9c3806459b17191c8 Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 15 Oct 2020 23:35:35 -0400 Subject: [PATCH 10/18] Refactored combine_settings function, default no fill, combine_settings_add_new to fill --- dizqueTV/helpers.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index eff5501..0bf5cad 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -32,31 +32,43 @@ def inner(obj, **kwargs): # Internal Helpers -def _combine_settings(new_settings_dict: dict, old_settings_dict: dict) -> dict: +def _combine_settings_add_new(new_settings_dict: dict, template_dict: dict, ignore_keys: List = None) -> dict: """ Build a complete dictionary for new settings, using old settings as a base Add new keys to template. :param new_settings_dict: Dictionary of new settings kwargs - :param old_settings_dict: Current settings + :param template_dict: Current settings + :param ignore_keys: List of keys to ignore when combining dictionaries :return: Dictionary of new settings """ + if not ignore_keys: + ignore_keys = [] for k, v in new_settings_dict.items(): - old_settings_dict[k] = v - return old_settings_dict + if k in ignore_keys: + pass + else: + template_dict[k] = v + return template_dict -def _combine_settings_by_template(new_settings_dict: dict, settings_template: dict) -> dict: +def _combine_settings(new_settings_dict: dict, template_dict: dict, ignore_keys: List = None) -> dict: """ Build a complete dictionary for new settings, using old settings as a base Do not add new keys to template. :param new_settings_dict: Dictionary of new settings kwargs - :param settings_template: settings template + :param template_dict: settings template + :param ignore_keys: List of keys to ignore when combining dictionaries :return: Dictionary of new settings """ + if not ignore_keys: + ignore_keys = [] for k, v in new_settings_dict.items(): - if k in settings_template.keys(): - settings_template[k] = v - return settings_template + if k in template_dict.keys(): + if k in ignore_keys: + pass + else: + template_dict[k] = v + return template_dict def _settings_are_complete(new_settings_dict: json, template_settings_dict: json, ignore_id: bool = False) -> bool: From 532c0b1092be310bc85a2a2ee9c24e3b7e02e0da Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 15 Oct 2020 23:39:02 -0400 Subject: [PATCH 11/18] Refactored settings_are_complete to customizable list of keys to ignore --- dizqueTV/channels.py | 4 ++-- dizqueTV/dizquetv.py | 8 ++++---- dizqueTV/fillers.py | 2 +- dizqueTV/helpers.py | 8 +++++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index 29ef100..af1ab7b 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -255,7 +255,7 @@ def add_program(self, template = REDIRECT_PROGRAM_TEMPLATE if helpers._settings_are_complete(new_settings_dict=kwargs, template_settings_dict=template, - ignore_id=True): + ignore_keys=['_id', 'id']): channel_data = self._data channel_data['programs'].append(kwargs) channel_data['duration'] += kwargs['duration'] @@ -445,7 +445,7 @@ def add_filler_list(self, } if helpers._settings_are_complete(new_settings_dict=new_settings_dict, template_settings_dict=FILLER_LIST_CHANNEL_TEMPLATE, - ignore_id=False): + ignore_keys=['_id', 'id']): channel_data = self._data channel_data['fillerCollections'].append(new_settings_dict) # filler_list_data['duration'] += kwargs['duration'] diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index e1abd26..e717a70 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -214,7 +214,7 @@ def add_plex_server(self, **kwargs) -> Union[PlexServer, None]: """ if helpers._settings_are_complete(new_settings_dict=kwargs, template_settings_dict=PLEX_SERVER_SETTINGS_TEMPLATE, - ignore_id=True) \ + ignore_keys=['_id', 'id']) \ and self._put(endpoint='/plex-servers', data=kwargs): return self.get_plex_server(server_name=kwargs['name']) return None @@ -315,7 +315,7 @@ def _fill_in_watermark_settings(self, handle_errors: bool = True, **kwargs) -> d :return: A complete and valid watermark dict """ final_dict = helpers._combine_settings_add_new(new_settings_dict=kwargs, - template_dict=WATERMARK_SETTINGS_DEFAULT) + template_dict=WATERMARK_SETTINGS_DEFAULT) if handle_errors and final_dict['enabled'] is True: if not (0 < final_dict['width'] <= 100): raise Exception("Watermark width must greater than 0 and less than 100") @@ -390,7 +390,7 @@ def add_channel(self, kwargs = self._fill_in_default_channel_settings(settings_dict=kwargs, handle_errors=handle_errors) if helpers._settings_are_complete(new_settings_dict=kwargs, template_settings_dict=CHANNEL_SETTINGS_TEMPLATE, - ignore_id=True) \ + ignore_keys=['_id', 'id']) \ and self._put(endpoint="/channel", data=kwargs): return self.get_channel(channel_number=kwargs['number']) return None @@ -511,7 +511,7 @@ def add_filler_list(self, kwargs = self._fill_in_default_filler_list_settings(settings_dict=kwargs, handle_errors=handle_errors) if helpers._settings_are_complete(new_settings_dict=kwargs, template_settings_dict=FILLER_LIST_SETTINGS_TEMPLATE, - ignore_id=True): + ignore_keys=['_id', 'id']): response = self._put(endpoint="/filler", data=kwargs) if response: return self.get_filler_list(filler_list_id=response.json()['id']) diff --git a/dizqueTV/fillers.py b/dizqueTV/fillers.py index 8bb4738..72d1f3d 100644 --- a/dizqueTV/fillers.py +++ b/dizqueTV/fillers.py @@ -100,7 +100,7 @@ def add_filler(self, kwargs = filler._data if helpers._settings_are_complete(new_settings_dict=kwargs, template_settings_dict=FILLER_ITEM_TEMPLATE, - ignore_id=True): + ignore_keys=['_id', 'id']): filler_list_data = self._data filler_list_data['content'].append(kwargs) filler_list_data['duration'] += kwargs['duration'] diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index 0bf5cad..d70d1db 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -71,18 +71,20 @@ def _combine_settings(new_settings_dict: dict, template_dict: dict, ignore_keys: return template_dict -def _settings_are_complete(new_settings_dict: json, template_settings_dict: json, ignore_id: bool = False) -> bool: +def _settings_are_complete(new_settings_dict: json, template_settings_dict: json, ignore_keys: List = None) -> bool: """ Check that all elements from the settings template are present in the new settings :param new_settings_dict: Dictionary of new settings kwargs :param template_settings_dict: Template of settings - :param ignore_id: Ignore if "_id" is not included in new_settings_dict + :param ignore_keys: List of keys to ignore when analyzing completeness Ignore if "_id" is not included in new_settings_dict :return: True if valid, raise dizqueTV.exceptions.IncompleteSettingsError if not valid """ + if not ignore_keys: + ignore_keys = [] for k in template_settings_dict.keys(): if k not in new_settings_dict.keys(): # or not isinstance(new_settings_dict[k], type(template_settings_dict[k])) - if k in ['_id', 'id'] and ignore_id: + if k in ignore_keys: pass else: raise MissingSettingsError(f"Missing setting: {k}") From 8192df9d9a90d5ca27e9b3f771577846c46b39c2 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 16 Oct 2020 00:13:30 -0400 Subject: [PATCH 12/18] Added fast_forward and rewind functions to Channel object to adjust start time --- dizqueTV/channels.py | 57 +++++++++++++++++++++++++++++++++++++++++++- dizqueTV/helpers.py | 22 +++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index af1ab7b..fef8d46 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -34,7 +34,6 @@ def json(self): """ return self._data - @helpers._check_for_dizque_instance def update(self, use_global_settings: bool = False, **kwargs) -> bool: """ @@ -836,6 +835,62 @@ def balance_programs(self, margin_of_error: float = 0.1) -> bool: return self.add_programs(programs=sorted_programs) return False + @helpers._check_for_dizque_instance + def fast_forward(self, + seconds: int = 0, + minutes: int = 0, + hours: int = 0, + days: int = 0, + months: int = 0, + years: int = 0) -> bool: + """ + Fast forward the channel start time by an amount of time + :param seconds: how many seconds + :param minutes: how many minutes + :param hours: how many hours + :param days: how many days + :param months: how many months (assume 30 days in month) + :param years: how many years (assume 365 days in year) + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + current_start_time = helpers.string_to_datetime(date_string=self.startTime) + shifted_start_time = helpers.shift_time(starting_time=current_start_time, + seconds=seconds, + minutes=minutes, + hours=hours, + days=days, + months=months, + years=years) + shifted_start_time = helpers.datetime_to_string(datetime_object=shifted_start_time) + if self.update(startTime=shifted_start_time): + return True + return False + + @helpers._check_for_dizque_instance + def rewind(self, + seconds: int = 0, + minutes: int = 0, + hours: int = 0, + days: int = 0, + months: int = 0, + years: int = 0) -> bool: + """ + Fast forward the channel start time by an amount of time + :param seconds: how many seconds + :param minutes: how many minutes + :param hours: how many hours + :param days: how many days + :param months: how many months (assume 30 days in month) + :param years: how many years (assume 365 days in year) + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + return self.fast_forward(seconds=seconds * -1, + minutes=minutes * -1, + hours=hours * -1, + days=days * -1, + months=months * -1, + years=years * -1) + # Delete @helpers._check_for_dizque_instance def delete(self) -> bool: diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index d70d1db..ae6da60 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -391,6 +391,28 @@ def hours_difference_in_timezone() -> int: return int((datetime.utcnow() - datetime.now()).total_seconds() / 60 / 60) +def shift_time(starting_time: datetime, + seconds: int = 0, + minutes: int = 0, + hours: int = 0, + days: int = 0, + months: int = 0, + years: int = 0) -> datetime: + """ + Shift a time forward or backwards + :param starting_time: datetime.datetime object + :param seconds: how many seconds + :param minutes: how many minutes + :param hours: how many hours + :param days: how many days + :param months: how many months (assume 30 days in month) + :param years: how many years (assume 365 days in year) + :return: shifted datetime.datetime object + """ + days = days + (30 * months) + (365 * years) + return starting_time + timedelta(seconds=seconds, minutes=minutes, hours=hours, days=days) + + def get_nearest_30_minute_mark() -> str: """ Get the most recently past hour or half-hour time From ac7113ca2f5a34e8e36643c9834337dd84d82744 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 16 Oct 2020 19:43:13 -0400 Subject: [PATCH 13/18] New TimeSlot and Schedule objects --- dizqueTV/channels.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index fef8d46..511de7a 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -100,6 +100,32 @@ def update(self, **kwargs) -> bool: return False +class TimeSlot: + def __init__(self, data: dict): + self._data = data + self.time = data.get('time') + self.showId = data.get('showId') + self.order = data.get('order') + + def __repr__(self): + return f"<{self.__class__.__name__}:{self.time}:{self.showId}:{self.order}>" + + +class Schedule: + def __init__(self, data: dict, dizque_instance, channel_instance): + self._data = data + self._dizque_instance = dizque_instance + self._channel_instance = channel_instance + self.lateness = data.get('lateness') + self.maxDays = data.get('maxDays') + self.slots = [TimeSlot(data=slot) for slot in data['slots']] + self.pad = data.get('pad') + self.timeZoneOffset = data.get('timeZoneOffset') + + def __repr__(self): + return f"<{self.__class__.__name__}:{self.maxDays} Days:{len(self.slots)} TimeSlots>" + + class Channel: def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self._data = data @@ -125,6 +151,9 @@ def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self.transcoding = ChannelFFMPEGSettings(data=data.get('transcoding'), dizque_instance=dizque_instance, channel_instance=self) + self.scheduleBackup = Schedule(data=data.get('scheduleBackup'), + dizque_instance=dizque_instance, + channel_instance=self) self.plex_server = plex_server def __repr__(self): From 103f9514b233bbfa1218efa27101c1c55b0e80ad Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 20 Oct 2020 00:35:13 -0400 Subject: [PATCH 14/18] New function to make time slot from program --- dizqueTV/__init__.py | 2 +- dizqueTV/dizquetv.py | 72 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/dizqueTV/__init__.py b/dizqueTV/__init__.py index ea424c0..1f2a430 100644 --- a/dizqueTV/__init__.py +++ b/dizqueTV/__init__.py @@ -1,3 +1,3 @@ from dizqueTV.dizquetv import API, convert_plex_item_to_filler_item, convert_plex_item_to_program, \ - convert_plex_server_to_dizque_plex_server + convert_plex_server_to_dizque_plex_server, make_time_slot_from_dizque_program from ._version import __author__, __version__ diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index e717a70..ed31fbe 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -11,7 +11,7 @@ import dizqueTV.requests as requests from dizqueTV.settings import XMLTVSettings, PlexSettings, FFMPEGSettings, HDHomeRunSettings -from dizqueTV.channels import Channel +from dizqueTV.channels import Channel, TimeSlot, TimeSlotItem, Schedule from dizqueTV.guide import Guide from dizqueTV.fillers import FillerList from dizqueTV.media import FillerItem, Program, Redirect @@ -22,6 +22,29 @@ from dizqueTV.exceptions import MissingParametersError, ChannelCreationError, ItemCreationError +def make_time_slot_from_dizque_program(program: Union[Program, Redirect], + time: str, + order: str) -> Union[TimeSlot, None]: + """ + Convert a DizqueTV Program or Redirect into a TimeSlot object for use in scheduling + :param program: Program or Redirect object + :param time: time for time slot + :param order: order ('shuffle' or 'next') for time slot + :return: TimeSlot object + """ + if program.type == 'redirect': + item = TimeSlotItem(item_type='redirect', item_value=program.channel) + elif program.showTitle: + if program.type == 'movie': + item = TimeSlotItem(item_type='movie', item_value=program.showTitle) + else: + item = TimeSlotItem(item_type='tv', item_value=program.showTitle) + else: + return None + data = {'time': helpers.convert_24_time_to_milliseconds_past_midnight(time_string=time), 'showId': item.showId, 'order': order} + return TimeSlot(data=data, program=item) + + def convert_plex_item_to_program(plex_item: Union[Video, Movie, Episode], plex_server: PServer) -> Program: """ Convert a PlexAPI Video, Movie or Episode object into a Program @@ -252,8 +275,7 @@ def update_plex_server(self, server_name: str, **kwargs) -> bool: """ server = self.get_plex_server(server_name=server_name) if server: - old_settings = server._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=server._data) if self._post(endpoint='/plex-servers', data=new_settings): return True return False @@ -404,10 +426,9 @@ def update_channel(self, channel_number: int, **kwargs) -> bool: """ channel = self.get_channel(channel_number=channel_number) if channel: - old_settings = channel._data if kwargs.get('iconPosition'): kwargs['iconPosition'] = helpers.convert_icon_position(position_text=kwargs['iconPosition']) - new_settings = helpers._combine_settings_add_new(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings_add_new(new_settings_dict=kwargs, template_dict=channel._data) if self._post(endpoint="/channel", data=new_settings): return True return False @@ -421,6 +442,32 @@ def delete_channel(self, channel_number: int) -> bool: return True return False + def _make_schedule(self, channel: Channel, schedule: Schedule = None, schedule_settings: dict = None) -> json: + """ + Add or update a schedule to a Channel + :param channel: Channel object to add schedule to + :param schedule: Schedule object to add (Optional) + :param schedule_settings: Schedule settings dictionary to use (Optional) + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + data = {'programs': []} + if schedule: + data['schedule'] = (schedule._data + if helpers._object_has_attribute(object=schedule, attribute_name="_data") + else {}) + else: + data['schedule'] = schedule_settings + for item in channel.programs: + if type(item) in [Program, Redirect]: + data['programs'].append(item._data) + res = self._post(endpoint='/channel-tools/time-slots', data=data) + if res: + schedule_json = res.json() + return channel.update(programs=schedule_json['programs'], + startTime=schedule_json['startTime'], + scheduleBackup=data['schedule']) + return False + # FillerItem List Settings @property def filler_lists(self) -> List[FillerList]: @@ -526,8 +573,7 @@ def update_filler_list(self, filler_list_id: str, **kwargs) -> bool: """ filler_list = self.get_filler_list(filler_list_id=filler_list_id) if filler_list: - old_settings = filler_list._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=filler_list._data) if self._post(endpoint=f"/filler/{filler_list_id}", data=new_settings): return True return False @@ -559,8 +605,7 @@ def update_ffmpeg_settings(self, **kwargs) -> bool: :param kwargs: keyword arguments of setting names and values :return: True if successful, False if unsuccessful """ - old_settings = self.ffmpeg_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=self.ffmpeg_settings._data) if self._put(endpoint='/ffmpeg-settings', data=new_settings): return True return False @@ -593,8 +638,7 @@ def update_plex_settings(self, **kwargs) -> bool: :param kwargs: keyword arguments of setting names and values :return: True if successful, False if unsuccessful """ - old_settings = self.plex_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=self.plex_settings._data) if self._put(endpoint='/plex-settings', data=new_settings): return True return False @@ -636,8 +680,7 @@ def update_xmltv_settings(self, **kwargs) -> bool: :param kwargs: keyword arguments of setting names and values :return: True if successful, False if unsuccessful """ - old_settings = self.xmltv_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=self.xmltv_settings._data) if self._put(endpoint='/xmltv-settings', data=new_settings): return True return False @@ -670,8 +713,7 @@ def update_hdhr_settings(self, **kwargs) -> bool: :param kwargs: keyword arguments of setting names and values :return: True if successful, False if unsuccessful """ - old_settings = self.hdhr_settings._data - new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=old_settings) + new_settings = helpers._combine_settings(new_settings_dict=kwargs, template_dict=self.hdhr_settings._data) if self._put(endpoint='/hdhr-settings', data=new_settings): return True return False From 98fcc14c3dc594a1a51065ff3e574666d3f09774 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 20 Oct 2020 00:35:31 -0400 Subject: [PATCH 15/18] Schedule editing --- dizqueTV/channels.py | 318 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 288 insertions(+), 30 deletions(-) diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index 511de7a..b8732b0 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -10,12 +10,15 @@ from dizqueTV.media import Redirect, Program, FillerItem from dizqueTV.templates import MOVIE_PROGRAM_TEMPLATE, EPISODE_PROGRAM_TEMPLATE, \ REDIRECT_PROGRAM_TEMPLATE, FILLER_LIST_SETTINGS_TEMPLATE, FILLER_LIST_CHANNEL_TEMPLATE, \ - CHANNEL_FFMPEG_SETTINGS_DEFAULT + CHANNEL_FFMPEG_SETTINGS_DEFAULT, SCHEDULE_SETTINGS_DEFAULT, TIME_SLOT_SETTINGS_TEMPLATE, SCHEDULE_SETTINGS_TEMPLATE from dizqueTV.exceptions import MissingParametersError class ChannelFFMPEGSettings: - def __init__(self, data: dict, dizque_instance, channel_instance): + def __init__(self, + data: dict, + dizque_instance, + channel_instance): self._data = data self._dizque_instance = dizque_instance self._channel_instance = channel_instance @@ -35,7 +38,9 @@ def json(self): return self._data @helpers._check_for_dizque_instance - def update(self, use_global_settings: bool = False, **kwargs) -> bool: + def update(self, + use_global_settings: bool = False, + **kwargs) -> bool: """ Edit this channel's FFMPEG settings on dizqueTV Automatically refreshes associated Channel object @@ -48,7 +53,7 @@ def update(self, use_global_settings: bool = False, **kwargs) -> bool: new_settings = CHANNEL_FFMPEG_SETTINGS_DEFAULT else: new_settings = helpers._combine_settings(new_settings_dict=kwargs, - template_dict=CHANNEL_FFMPEG_SETTINGS_DEFAULT) + template_dict=self._data) if self._dizque_instance.update_channel(channel_number=self._channel_instance.number, transcoding=new_settings): self._channel_instance.refresh() @@ -58,7 +63,10 @@ def update(self, use_global_settings: bool = False, **kwargs) -> bool: class Watermark: - def __init__(self, data: dict, dizque_instance, channel_instance): + def __init__(self, + data: dict, + dizque_instance, + channel_instance): self._data = data self._dizque_instance = dizque_instance self._channel_instance = channel_instance @@ -84,7 +92,8 @@ def json(self): return self._data @helpers._check_for_dizque_instance - def update(self, **kwargs) -> bool: + def update(self, + **kwargs) -> bool: """ Edit this Watermark on dizqueTV Automatically refreshes associated Channel object @@ -100,34 +109,173 @@ def update(self, **kwargs) -> bool: return False +class TimeSlotItem: + def __init__(self, + item_type: str, + item_value: str = ""): + self.showId = f"{item_type}.{item_value}" + + def __repr__(self): + return f"<{self.__class__.__name__}:{self.showId}>" + + class TimeSlot: - def __init__(self, data: dict): + def __init__(self, + data: dict, + program: TimeSlotItem = None, + schedule_instance=None): self._data = data self.time = data.get('time') - self.showId = data.get('showId') + self.showId = (program.showId if program else data.get('showId')) self.order = data.get('order') + self._schedule_instance = schedule_instance def __repr__(self): return f"<{self.__class__.__name__}:{self.time}:{self.showId}:{self.order}>" + def edit(self, time_string: str = None, **kwargs) -> bool: + """ + Edit this TimeSlot object + :param time_string: time in readable 24-hour format + (ex. 00:00:00 = 12:00:00 A.M., 05:15:00 = 5:15 A.M., 20:08:12 = 8:08:12 P.M.) + (Optional if time= not included in kwargs) + :param kwargs: Keyword arguments for the edited time slot (time, showId and order) + :return: True if successful, False if unsuccessful (Channel reloads in-place, + this TimeSlot and its parent Schedule object are destroyed) + """ + if not self._schedule_instance: + return False + return self._schedule_instance.edit_time_slot(time_slot=self, time_string=time_string, **kwargs) + + def delete(self) -> bool: + """ + Delete this TimeSlot object from the schedule + :return: True if successful, False if unsuccessful (Channel reloads in-place, + this TimeSlot and its parent Schedule object are destroyed) + """ + if not self._schedule_instance: + return False + return self._schedule_instance.delete_time_slot(time_slot=self) + class Schedule: - def __init__(self, data: dict, dizque_instance, channel_instance): + def __init__(self, + data: dict, + dizque_instance, + channel_instance): self._data = data self._dizque_instance = dizque_instance self._channel_instance = channel_instance self.lateness = data.get('lateness') self.maxDays = data.get('maxDays') - self.slots = [TimeSlot(data=slot) for slot in data['slots']] + self.slots = [TimeSlot(data=slot, schedule_instance=self) for slot in data.get('slots', [])] self.pad = data.get('pad') self.timeZoneOffset = data.get('timeZoneOffset') def __repr__(self): return f"<{self.__class__.__name__}:{self.maxDays} Days:{len(self.slots)} TimeSlots>" + @helpers._check_for_dizque_instance + def update(self, + **kwargs): + """ + Edit this Schedule on dizqueTV + Automatically refreshes associated Channel object + :param kwargs: keyword arguments of Schedule settings names and values + :return: True if successful, False if unsuccessful (Channel reloads in-place, this Schedule object is destroyed) + """ + new_settings = helpers._combine_settings_add_new(new_settings_dict=kwargs, + template_dict=(self._data + if self._data else SCHEDULE_SETTINGS_DEFAULT) + ) + return self._channel_instance.update_schedule(**new_settings) + + def add_time_slot(self, + time_slot: TimeSlot = None, + time_string: str = None, + **kwargs) -> bool: + """ + Add a time slot to this Schedule + :param time_slot: TimeSlot object to add (Optional) + :param time_string: time in readable 24-hour format + (ex. 00:00:00 = 12:00:00 A.M., 05:15:00 = 5:15 A.M., 20:08:12 = 8:08:12 P.M.) + (Optional if time= not included in kwargs) + :param kwargs: keyword arguments for a new time slot (time, showId and order) + :return: True if successful, False if unsuccessful (Channel reloads in-place, this Schedule object is destroyed) + """ + if time_slot: + kwargs = time_slot._data + else: + if time_string: + kwargs['time'] = helpers.convert_24_time_to_milliseconds_past_midnight(time_string=time_string) + new_settings_filtered = helpers._filter_dictionary(new_dictionary=kwargs, + template_dict=TIME_SLOT_SETTINGS_TEMPLATE) + if not helpers._settings_are_complete(new_settings_dict=new_settings_filtered, + template_settings_dict=TIME_SLOT_SETTINGS_TEMPLATE): + raise Exception("Missing settings required to make a time slot.") + + kwargs = new_settings_filtered + if kwargs['showId'] not in [item.showId for item in self._channel_instance.scheduledableItems]: + raise Exception(f"Program {kwargs['showId']} cannot be added to a time slot. " + f"Please make sure the program is added to the channel first.") + slots = self._data.get('slots', []) + if kwargs['time'] in [slot['time'] for slot in slots]: + raise Exception(f"Time slot {kwargs['time']} is already filled.") + slots.append(kwargs) + return self.update(slots=slots) + + def edit_time_slot(self, time_slot: TimeSlot, time_string: str = None, **kwargs) -> bool: + """ + Edit a time slot from this Schedule + :param time_slot: TimeSlot object to edit + :param time_string: time in readable 24-hour format + (ex. 00:00:00 = 12:00:00 A.M., 05:15:00 = 5:15 A.M., 20:08:12 = 8:08:12 P.M.) + (Optional if time= not included in kwargs) + :param kwargs: Keyword arguments for the edited time slot (time, showId and order) + :return: True if successful, False if unsuccessful (Channel reloads in-place, this Schedule object is destroyed) + """ + if time_string: + kwargs['time'] = helpers.convert_24_time_to_milliseconds_past_midnight(time_string=time_string) + current_slots = self._data.get('slots', []) + new_slots = [] + for slot in current_slots: + if slot['time'] != time_slot.time: + new_slots.append(slot) + else: + new_slot_data = helpers._combine_settings(new_settings_dict=kwargs, template_dict=slot) + new_slots.append(new_slot_data) + return self.update(slots=new_slots) + + @helpers._check_for_dizque_instance + def delete_time_slot(self, time_slot: TimeSlot) -> bool: + """ + Delete a time slot from this Schedule + :param time_slot: TimeSlot object to remove + :return: True if successful, False if unsuccessful (Channel reloads in-place, this Schedule object is destroyed) + """ + slots = self._data.get('slots', []) + try: + slots.remove(time_slot._data) + return self.update(slots=slots) + except ValueError: + pass + return False + + @helpers._check_for_dizque_instance + def delete(self) -> bool: + """ + Delete this channel's Schedule + Removes all duplicate programs, adds random shuffle + :return: True if successful, False if unsuccessful (Channel reloads in-place, this Schedule object is destroyed) + """ + return self._channel_instance.delete_schedule() + class Channel: - def __init__(self, data: json, dizque_instance, plex_server: PServer = None): + def __init__(self, + data: json, + dizque_instance, + plex_server: PServer = None): self._data = data self._dizque_instance = dizque_instance self._program_data = data.get('programs') @@ -151,14 +299,30 @@ def __init__(self, data: json, dizque_instance, plex_server: PServer = None): self.transcoding = ChannelFFMPEGSettings(data=data.get('transcoding'), dizque_instance=dizque_instance, channel_instance=self) - self.scheduleBackup = Schedule(data=data.get('scheduleBackup'), - dizque_instance=dizque_instance, - channel_instance=self) + self.schedule = Schedule(data=data.get('scheduleBackup'), + dizque_instance=dizque_instance, + channel_instance=self) self.plex_server = plex_server + self.scheduledableItems = self._get_schedulable_items() def __repr__(self): return f"<{self.__class__.__name__}:{self.number}:{self.name}>" + def _get_schedulable_items(self) -> List[TimeSlotItem]: + used_titles = [] + schedulable_items = [] + for program in self.programs: + if program.type == 'redirect' and program.channel not in used_titles: + schedulable_items.append(TimeSlotItem(item_type='redirect', item_value=program.channel)) + used_titles.append(program.channel) + elif program.showTitle and program.showTitle not in used_titles: + if program.type == 'movie': + schedulable_items.append(TimeSlotItem(item_type='movie', item_value=program.showTitle)) + else: + schedulable_items.append(TimeSlotItem(item_type='tv', item_value=program.showTitle)) + used_titles.append(program.showTitle) + return schedulable_items + # CRUD Operations # Create (handled in dizqueTV.py) # Read @@ -172,7 +336,9 @@ def programs(self): for program in self._program_data] @helpers._check_for_dizque_instance - def get_program(self, program_title: str = None, redirect_channel_number: int = None) -> Union[Program, None]: + def get_program(self, + program_title: str = None, + redirect_channel_number: int = None) -> Union[Program, None]: """ Get a specific program on this channel :param program_title: Title of program @@ -197,7 +363,8 @@ def filler_lists(self): for filler_list in self._fillerCollections_data] @helpers._check_for_dizque_instance - def get_filler_list(self, filler_list_title: str) -> Union[FillerList, None]: + def get_filler_list(self, + filler_list_title: str) -> Union[FillerList, None]: """ Get a specific filler list on this channel :param filler_list_title: Title of filler list @@ -230,7 +397,8 @@ def refresh(self): del temp_channel @helpers._check_for_dizque_instance - def update(self, **kwargs) -> bool: + def update(self, + **kwargs) -> bool: """ Edit this Channel on dizqueTV Automatically refreshes current Channel object @@ -243,7 +411,8 @@ def update(self, **kwargs) -> bool: return False @helpers._check_for_dizque_instance - def edit(self, **kwargs) -> bool: + def edit(self, + **kwargs) -> bool: """ Alias for channels.update() :param kwargs: keyword arguments of Channel settings names and values @@ -291,7 +460,9 @@ def add_program(self, return False @helpers._check_for_dizque_instance - def add_programs(self, programs: List[Union[Program, Video, Movie, Episode]], plex_server: PServer = None) -> bool: + def add_programs(self, + programs: List[Union[Program, Video, Movie, Episode]], + plex_server: PServer = None) -> bool: """ Add multiple programs to this channel :param programs: List of Program, plexapi.video.Video, plexapi.video.Movie or plexapi.video.Episode objects @@ -317,7 +488,8 @@ def add_programs(self, programs: List[Union[Program, Video, Movie, Episode]], pl return self.update(**channel_data) @helpers._check_for_dizque_instance - def delete_program(self, program: Program) -> bool: + def delete_program(self, + program: Program) -> bool: """ Delete a program from this channel :param program: Program object to delete @@ -333,7 +505,9 @@ def delete_program(self, program: Program) -> bool: return False @helpers._check_for_dizque_instance - def delete_show(self, show_name: str, season_number: int = None) -> bool: + def delete_show(self, + show_name: str, + season_number: int = None) -> bool: """ Delete all episodes of a specific show :param show_name: Name of show to delete @@ -481,10 +655,13 @@ def add_filler_list(self, return False @helpers._check_for_dizque_instance - def delete_filler_list(self, filler_list: FillerList = None, filler_list_id: str = None) -> bool: + def delete_filler_list(self, + filler_list: FillerList = None, + filler_list_id: str = None) -> bool: """ Delete a program from this channel :param filler_list: FillerList object to delete + :param filler_list_id: ID of filler list to delete :return: True if successful, False if unsuccessful (Channel reloads in-place) """ if not filler_list and not filler_list_id: @@ -508,6 +685,46 @@ def delete_all_filler_lists(self): channel_data['fillerCollections'] = [] return self.update(**channel_data) + # Schedule + @helpers._check_for_dizque_instance + def add_schedule(self, time_slots: List[TimeSlot], **kwargs) -> bool: + """ + Add a schedule to this channel + :param time_slots: List of TimeSlot objects + :param kwargs: keyword arguments for schedule settings + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + for slot in time_slots: + kwargs['slots'].append(slot._data) + schedule_settings = helpers._combine_settings(new_settings_dict=kwargs, + template_dict=SCHEDULE_SETTINGS_DEFAULT) + if helpers._settings_are_complete(new_settings_dict=schedule_settings, + template_settings_dict=SCHEDULE_SETTINGS_TEMPLATE): + schedule = Schedule(data=schedule_settings, dizque_instance=None, channel_instance=self) + return self._dizque_instance._make_schedule(channel=self, schedule=schedule) + return False + + @helpers._check_for_dizque_instance + def update_schedule(self, **kwargs) -> bool: + """ + Update the schedule for this channel + :param kwargs: keyword arguments for schedule settings (slots data included if needed) + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + return self.add_schedule(time_slots=[], **kwargs) + + @helpers._check_for_dizque_instance + def delete_schedule(self) -> bool: + """ + Delete this channel's Schedule + Removes all offline times, removes duplicate programs (and all redirects), random shuffles remaining items + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + self._delete_all_offline_times() + if self.remove_duplicate_programs(): # also removes all redirects + self.sort_programs_randomly() + return self.update(scheduleBackup={}) + # Sort Programs @helpers._check_for_dizque_instance def sort_programs_by_release_date(self) -> bool: @@ -577,7 +794,9 @@ def cyclical_shuffle(self) -> bool: return False @helpers._check_for_dizque_instance - def block_shuffle(self, block_length: int, randomize: bool = False) -> bool: + def block_shuffle(self, + block_length: int, + randomize: bool = False) -> bool: """ Sort TV shows on this channel cyclically :param block_length: Length of each block @@ -592,7 +811,8 @@ def block_shuffle(self, block_length: int, randomize: bool = False) -> bool: return False @helpers._check_for_dizque_instance - def replicate(self, how_many_times: int) -> bool: + def replicate(self, + how_many_times: int) -> bool: """ Replicate/repeat the channel lineup x number of times Items will remain in the same order. @@ -610,7 +830,8 @@ def replicate(self, how_many_times: int) -> bool: return False @helpers._check_for_dizque_instance - def replicate_and_shuffle(self, how_many_times: int) -> bool: + def replicate_and_shuffle(self, + how_many_times: int) -> bool: """ Replicate/repeat the channel lineup, shuffled, x number of times Items will be shuffled in each repeat group. @@ -633,6 +854,7 @@ def replicate_and_shuffle(self, how_many_times: int) -> bool: def remove_duplicate_programs(self) -> bool: """ Delete duplicate programs on this channel + NOTE: Removes all redirects :return: True if successful, False if unsuccessful (Channel reloads in-place) """ sorted_programs = helpers.remove_duplicate_media_items(media_items=self.programs) @@ -640,6 +862,31 @@ def remove_duplicate_programs(self) -> bool: return self.add_programs(programs=sorted_programs) return False + @helpers._check_for_dizque_instance + def remove_duplicate_redirects(self) -> bool: + """ + Delete duplicate redirects on this channel + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + sorted_programs = helpers.remove_duplicates_by_attribute(items=self.programs, attribute_name='channel') + if sorted_programs and self.delete_all_programs(): + return self.add_programs(programs=sorted_programs) + return False + + @helpers._check_for_dizque_instance + def remove_all_redirects(self) -> bool: + """ + Delete all redirects from a channel, preserving offline times, programs and filler items + :return: True if successful, False if unsuccessful (Channel reloads in-place) + """ + non_redirects = [] + for item in self.programs: + if not helpers._object_has_attribute(object=item, attribute_name='type') or item.type != 'redirect': + non_redirects.append(item) + if non_redirects and self.delete_all_programs(): + return self.add_programs(programs=non_redirects) + return False + @helpers._check_for_dizque_instance def remove_specials(self) -> bool: """ @@ -658,7 +905,8 @@ def remove_specials(self) -> bool: return False @helpers._check_for_dizque_instance - def pad_times(self, start_every_x_minutes: int) -> bool: + def pad_times(self, + start_every_x_minutes: int) -> bool: """ Add padding between programs on a channel, so programs start at specific intervals :param start_every_x_minutes: Programs start every X minutes past the hour @@ -685,7 +933,10 @@ def pad_times(self, start_every_x_minutes: int) -> bool: return False @helpers._check_for_dizque_instance - def add_reruns(self, start_time: datetime, length_hours: int, times_to_repeat: int) -> bool: + def add_reruns(self, + start_time: datetime, + length_hours: int, + times_to_repeat: int) -> bool: """ Add a block of reruns to a dizqueTV channel :param start_time: datetime.datetime object, what time the reruns start @@ -714,7 +965,10 @@ def add_reruns(self, start_time: datetime, length_hours: int, times_to_repeat: i return False @helpers._check_for_dizque_instance - def add_channel_at_night(self, night_channel_number: int, start_hour: int, end_hour: int) -> bool: + def add_channel_at_night(self, + night_channel_number: int, + start_hour: int, + end_hour: int) -> bool: """ Add a Channel at Night to a dizqueTV channel :param night_channel_number: number of the channel to redirect to @@ -766,7 +1020,10 @@ def add_channel_at_night(self, night_channel_number: int, start_hour: int, end_h return False @helpers._check_for_dizque_instance - def add_channel_at_night_alt(self, night_channel_number: int, start_hour: int, end_hour: int) -> bool: + def add_channel_at_night_alt(self, + night_channel_number: int, + start_hour: int, + end_hour: int) -> bool: if start_hour > 23 or start_hour < 0: raise Exception("start_hour must be between 0 and 23.") if end_hour > 23 or end_hour < 0: @@ -851,7 +1108,8 @@ def add_channel_at_night_alt(self, night_channel_number: int, start_hour: int, e return False @helpers._check_for_dizque_instance - def balance_programs(self, margin_of_error: float = 0.1) -> bool: + def balance_programs(self, + margin_of_error: float = 0.1) -> bool: """ Balance shows to the shortest show length. Movies unaffected. :param margin_of_error: (Optional) Specify margin of error when deciding whether to add another episode. From 1e924997c5c6c2667a79f0f2b03042743424fde3 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 20 Oct 2020 00:36:21 -0400 Subject: [PATCH 16/18] Time conversion, dictionary filtering, program list filtering --- dizqueTV/helpers.py | 78 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index ae6da60..62eb97c 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -71,12 +71,26 @@ def _combine_settings(new_settings_dict: dict, template_dict: dict, ignore_keys: return template_dict +def _filter_dictionary(new_dictionary: dict, template_dict: dict) -> dict: + """ + Remove key-value pairs from new_dictionary that are not present in template_dict + :param new_dictionary: Dictionary of key-value pairs + :param template_dict: Dictionary of accepted key-value pairs + :return: Dictionary with only accepted key-value pairs + """ + final_dict = {} + for k, v in new_dictionary.items(): + if k in template_dict.keys(): + final_dict[k] = v + return final_dict + + def _settings_are_complete(new_settings_dict: json, template_settings_dict: json, ignore_keys: List = None) -> bool: """ Check that all elements from the settings template are present in the new settings :param new_settings_dict: Dictionary of new settings kwargs :param template_settings_dict: Template of settings - :param ignore_keys: List of keys to ignore when analyzing completeness Ignore if "_id" is not included in new_settings_dict + :param ignore_keys: List of keys to ignore when analyzing completeness :return: True if valid, raise dizqueTV.exceptions.IncompleteSettingsError if not valid """ if not ignore_keys: @@ -353,7 +367,7 @@ def get_year_from_date(date_string: Union[datetime, str]) -> int: def string_to_datetime(date_string: str, template: str = "%Y-%m-%dT%H:%M:%S") -> datetime: """ - Convert a string to a datetime.datetime object + Convert a datetime string to a datetime.datetime object :param date_string: datetime string to convert :param template: (Optional) datetime template to use when parsing string :return: datetime.datetime object @@ -373,6 +387,28 @@ def datetime_to_string(datetime_object: datetime, template: str = "%Y-%m-%dT%H:% return datetime_object.strftime(template) +def string_to_time(time_string: str, template: str = "%H:%M:%S") -> datetime: + """ + Convert a time string to a datetime.datetime object + :param time_string: datetime string to convert + :param template: (Optional) datetime template to use when parsing string + :return: datetime.datetime object + """ + if time_string.endswith('Z'): + time_string = time_string[:-5] + return datetime.strptime(time_string, template) + + +def time_to_string(datetime_object: datetime, template: str = "%H:%M:%S") -> str: + """ + Convert a datetime.datetime object to a string + :param datetime_object: datetime.datetime object to convert + :param template: (Optional) datetime template to use when parsing string + :return: str representation of datetime + """ + return datetime_object.strftime(template) + + def adjust_datetime_for_timezone(local_time: datetime) -> datetime: """ Shift datetime.datetime in regards to UTC time @@ -426,6 +462,22 @@ def get_nearest_30_minute_mark() -> str: return now.strftime("%Y-%m-%dT%H:%M:%S.000Z") +def convert_24_time_to_milliseconds_past_midnight(time_string: str) -> int: + """ + Get milliseconds between time_string and midnight + :param time_string: readable 24-hour time (ex. 00:00:00, 05:30:15, 20:08:30) + :return: int of milliseconds since midnight + """ + hour_minute_second = time_string.split(':') + if len(hour_minute_second) < 2 or len(hour_minute_second) > 4: + raise Exception("Time string must be in two-digit format hour:minute:second") + if len(hour_minute_second) == 2: + time_string += ":00" + time_in_datetime = string_to_time(time_string=time_string) + midnight = string_to_time(time_string="00:00:00") + return get_milliseconds_between_two_datetimes(start_datetime=midnight, end_datetime=time_in_datetime) + + def get_milliseconds_between_two_hours(start_hour: int, end_hour: int) -> int: """ Get how many milliseconds between two 24-hour hours @@ -589,7 +641,9 @@ def remove_duplicates_by_attribute(items: List, attribute_name: str) -> List: filtered_attr = [] for item in items: attr = getattr(item, attribute_name) - if attr not in filtered_attr: + if not attr: + filtered.append(item) + elif attr not in filtered_attr: filtered.append(item) filtered_attr.append(attr) return filtered @@ -788,7 +842,6 @@ def balance_shows(media_items: List[Union[Program, FillerItem]], margin_of_corre shortest_show_length = min(show_durations) margin = 1 + margin_of_correction final_shows = [] - print(ordered_show_dict_with_durations) for show_name, show_data in ordered_show_dict_with_durations.items(): show_running_duration = 0 continue_with_show = True @@ -809,7 +862,18 @@ def balance_shows(media_items: List[Union[Program, FillerItem]], margin_of_corre return sorted_all -def remove_duplicate_media_items(media_items: List[Union[Program, FillerItem]]) -> List[Union[Program, FillerItem]]: +def remove_non_programs(media_items: List[Union[Program, Redirect, FillerItem]]) -> List[Union[Program, FillerItem]]: + """ + Remove all non-programs from list of media items. + :param media_items: List of Program, Redirect and FillerItem objects + :return: List of Program and FillerItem objects + """ + return [item for item in media_items if + (_object_has_attribute(object=item, attribute_name='type') + and item.type != 'redirect')] + + +def remove_duplicate_media_items(media_items: List[Union[Program, Redirect, FillerItem]]) -> List[Union[Program, FillerItem]]: """ Remove duplicate items from list of media items. Check by ratingKey. @@ -817,9 +881,7 @@ def remove_duplicate_media_items(media_items: List[Union[Program, FillerItem]]) :param media_items: List of Program and FillerItem objects :return: List of Program and FillerItem objects """ - non_redirects = [item for item in media_items if - (_object_has_attribute(object=item, attribute_name='type') - and item.type != 'redirect')] + non_redirects = remove_non_programs(media_items=media_items) return remove_duplicates_by_attribute(items=non_redirects, attribute_name='ratingKey') From b472eab966e44f4087a93e8abafbaeee2c881001 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 20 Oct 2020 00:36:40 -0400 Subject: [PATCH 17/18] TimeSlot and Schedule templates --- dizqueTV/templates.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/dizqueTV/templates.py b/dizqueTV/templates.py index 8940c7e..f38f6a4 100644 --- a/dizqueTV/templates.py +++ b/dizqueTV/templates.py @@ -46,6 +46,28 @@ "videoBufSize": None } +TIME_SLOT_SETTINGS_TEMPLATE = { + "time": int, + "showId": str, + "order": str +} + +SCHEDULE_SETTINGS_TEMPLATE = { + "lateness": int, + "maxDays": int, + "slots": [], + "pad": int, + "timeZoneOffset": int +} + +SCHEDULE_SETTINGS_DEFAULT = { + "lateness": 0, + "maxDays": 365, + "slots": [], + "pad": 1, + "timeZoneOffset": 0 +} + CHANNEL_SETTINGS_TEMPLATE = { "programs": List, "fillerRepeatCooldown": int, From 0e2a661c78ae0735566f9f6bd441ea18da6afbcd Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 20 Oct 2020 01:12:36 -0400 Subject: [PATCH 18/18] Documentation for v1.2.0.0 --- README.md | 76 ++++++++++++++++++++++++++++++++++++++------ dizqueTV/_version.py | 2 +- dizqueTV/channels.py | 2 +- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c172310..2755a84 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,16 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - Update a channel: ``updated = dtv.update_channel(channel_number: int, **kwargs)`` or ``Channel.update(**kwargs)`` -> True/False - Delete a channel: ``deleted = dtv.delete_channel(channel_number: int)`` or ``Channel.delete()`` -> True/False - Refresh a channel: ``Channel.refresh()`` -> None (reloads ``Channel`` object in-place) - - +- Edit channel's watermark: ``edited = Watermark.update(**kwargs)`` -> True/False +- Edit channel's FFMPEG settings: ``edited = ChannelFFMPEGSettings.update(**kwargs)`` -> True/False #### Programs - Get a channel's programs: ``programs = Channel.programs`` -> list of ``MediaItem`` objects - Add program (or PlexAPI Video) to a channel: ``added = Channel.add_program(plex_item: PlexAPI Video, plex_server: PlexAPI Server, program: Program, **kwargs)`` -> True/False - Add multiple programs (or PlexAPI Video) to a channel: ``added = Channel.add_fillers(programs: [Program, PlexAPI Video, ...], plex_server: PlexAPI Server)`` -> True/False - Add multiple programs to multiple channels: ``added = dtv.add_programs_to_channels(programs: [Program], channels: [Channel], channel_numbers: [int])`` -> True/False +- Add X number of show episodes: ``added = Channel.add_x_number_of_show_episodes(number_of_episodes: int, list_of_episodes: [Program, Episode, ...], plex_server: PServer (Optional))`` -> True/False +- Add X duration of show episodes: ``added = Channel.add_x_duration_of_show_episodes(duration_in_milliseconds: int, list_of_episodes: [Program, Episode, ...], plex_server: PServer (Optional), allow_overtime: bool)`` -> True/False - Delete a program: ``deleted = Channel.delete_program(program: Program)`` -> True/False - Delete all programs: ``deleted = Channel.delete_all_programs()`` -> True/False - Delete all episodes of a show (or of a season): ``deleted = Channel.delete_show(show_name: str, season_number: int (Optional))`` -> True/False @@ -66,10 +68,13 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - Repeat and shuffle programs ``repeated = Channel.replicate_and_shuffle(how_many_times: int)`` -> True/False - Remove duplicate programs: ``sorted = Channel.remove_duplicate_programs()`` -> True/False - Remove specials: ``sorted = Channel.remove_specials()`` -> True/False +- Remove redirects: ``sorted = Channel.remove_redirects()`` -> True/False - Balance shows: ``balanced = Channel.balance_programs(margin_of_error: float)`` -> True/False - Add pad times: ``added = Channel.pad_times(start_every_x_minutes: int)`` -> True/False - Add reruns: ``added = Channel.add_reruns(start_time: datetime.datetime, length_hours: int, times_to_repeat: int)`` -> True/False - Add "Channel at Night": ``added = Channel.add_channel_at_night(night_channel_number: int, start_hour: int (24-hour time), end_hour: int (24-hour time))`` -> True/False +- Fast forward: ``success = Channel.fast_forward(seconds: int, minutes: int, hours: int, days: int, months: int, years: int)`` -> True/False +- Rewind: ``success = Channel.rewind(seconds: int, minutes: int, hours: int, days: int, months: int, years: int)`` -> True/False #### Filler (Flex) - Get a filler list: ``filler_list = dtv.get_filler_list(filler_list_id: str)`` -> ``FillerList`` object @@ -93,6 +98,15 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - Sort filler items randomly: ``sorted = FillerList.sort_filler_randomly()`` -> True/False - Remove duplicate filler items: ``sorted = FillerList.remove_duplicate_fillers()`` -> True/False +#### Scheduling +- Get a channel's schedule: ``schedule = Channel.schedule`` -> ``Schedule`` object +- Add a schedule to a channel: ``added = Channel.add_schedule(time_slots: [TimeSlot, ...], **kwargs)`` -> True/False +- Update a channel's schedule: ``updated = Channel.update_schedule(**kwargs)`` or ``Schedule.update(**kwargs)`` -> True/False +- Delete a channel's schedule: ``deleted = Channel.delete_schedule()`` or ``Schedule.delete()`` -> True/False +- Add a time slot to a schedule: ``added = Schedule.add_time_slot(time_slot: TimeSlot, time_string: str, **kwargs)`` -> True/False +- Edit a time slot on a schedule: ``edited = Schedule.edit_time_slot(time_slot: TimeSlot, time_string: str, **kwargs)`` or ``TimeSlot.edit(time_string: str, **kwargs)`` -> True/False +- Delete a time slot on a schedule: ``deleted = Schedule.delete_time_slot(time_slot: TimeSlot)`` or ``TimeSlot.delete()`` -> True/False + #### Plex - Get all Plex Media Servers: ``servers = dtv.plex_servers`` -> list of ``PlexServer`` objects - Get a specific Plex Media Server: ``server = dtv.get_plex_server(server_name: str)`` -> ``PlexServer`` object @@ -142,6 +156,7 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - Convert a Python PlexAPI Video to a Program: ``program = dtv.convert_plex_item_to_program(plex_item: PlexAPI Video, plex_server: PlexAPI Server)`` or ``program = Channel.convert_plex_item_to_program(plex_item: PlexAPI Video, plex_server: PlexAPI Server)`` -> Program - Convert a Python PlexAPI Video to a Filler: ``filler = dtv.convert_plex_item_to_filler(plex_item: PlexAPI Video, plex_server: PlexAPI Server)`` or ``filler = Channel.convert_plex_item_to_filler(plex_item: PlexAPI Video, plex_server: PlexAPI Server)`` -> Program - Convert a Python PlexAPI Server to a PlexServer: ``server = dtv.convert_plex_server_to_dizque_plex_server(plex_server: PlexAPI Server)`` -> PlexServer +- Convert a DizqueTV Program or Redirect to a TimeSlot: ``time_slot = dtv.make_time_slot_from_dizque_program(program: Union[Program, Redirect], time: str, order: str)`` -> ``TimeSlot`` object - Repeat a list: ``repeated_list = dtv.repeat_list(items: List, how_many_times: int)`` -> List - Repeat and shuffle a list: ``repeated_list = dtv.repeate_and_shuffle_list(items: List, how_many_times: int)`` -> List @@ -167,15 +182,11 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla #### Channel - ``programs``: list of ``Program`` objects -- ``filler``: list of ``Filler`` objects +- ``filler_lists``: List of ``FillerList`` objects - ``fillerRepeatCooldown``: int -- ``fallback``: List of ``Filler`` objects +- ``fallback``: List of ``FillerItem`` objects - ``icon``: str(url) - ``disableFillerOverlay``: bool -- ``iconWidth``: int -- ``iconDuration``: int -- ``iconPosition``: str(int) -- ``overlayIcon``: bool - ``startTime``: str(datetime) - ``offlinePicture``: str(url) - ``offlineSoundtrack``: str(url) @@ -183,7 +194,39 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - ``number``: int - ``name``: str - ``duration``: int +- ``watermark``: ``Watermark`` object +- ``transcoding``: ``ChannelFFMPEGSettings`` object +- ``schedule``: ``Schedule`` object +- ``schedulableItems``: List of ``TimeSlotItem`` objects - ``stealth``: bool +- ``json``: JSON + +#### Watermark +- ``enabled``: bool +- ``width``: float +- ``verticalMargin``: float +- ``horizontalMargin``: float +- ``duration``: int +- ``fixedSize``: bool +- ``position``: str +- ``url``: str +- ``animated``: bool +- ``json``: JSON + +#### Schedule +- ``lateness``: int +- ``maxDays``: int +- ``slots``: List of ``TimeSlot`` objects +- ``pad``: int +- ``timeZoneOffset``: int + +#### TimeSlot +- ``time``: int +- ``showId``: str +- ``order``: str + +#### TimeSlotItem +- ``showId``: str #### Program - ``title``: str @@ -207,7 +250,15 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - ``serverKey``: str - ``isOffline``: false -#### Filler +#### FillerList +- ``id``: str +- ``name``: str +- ``count``: int +- ``details``: JSON +- ``content``: List of ``FillerItem`` objects +- ``channels``: List of ``Channel`` objects + +#### FillerItem - ``title``: str - ``key``: str - ``ratingKey``: str(int) @@ -291,6 +342,13 @@ Enable verbose logging by passing ``verbose=True`` into the ``API`` object decla - ``normalizeAudioCodec``: bool - ``normalizeResolution``: bool - ``normalizeAudio``: bool +- ``maxFPS``: int + +#### ChannelFFMPEGSettings +- ``targetResolution``: str +- ``videoBitrate``: int +- ``videoBufSize``: int +- ``json``: JSON #### HDHomeRunSettings - ``tunerCount``: int diff --git a/dizqueTV/_version.py b/dizqueTV/_version.py index f57c5db..99398bb 100644 --- a/dizqueTV/_version.py +++ b/dizqueTV/_version.py @@ -1,2 +1,2 @@ -__version__ = '1.1.1.1' +__version__ = '1.2.0.0' __author__ = 'Nate Harris' diff --git a/dizqueTV/channels.py b/dizqueTV/channels.py index b8732b0..cf5cbad 100644 --- a/dizqueTV/channels.py +++ b/dizqueTV/channels.py @@ -874,7 +874,7 @@ def remove_duplicate_redirects(self) -> bool: return False @helpers._check_for_dizque_instance - def remove_all_redirects(self) -> bool: + def remove_redirects(self) -> bool: """ Delete all redirects from a channel, preserving offline times, programs and filler items :return: True if successful, False if unsuccessful (Channel reloads in-place)