From 955e417015d5c452f837c2b92bb097deb2b295a7 Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Wed, 16 Feb 2022 23:28:52 -0700 Subject: [PATCH 1/4] - Added functions to get highest and lowest active channel numbers, lowest available channel number - Added functions to add collection, playlist to channel - Allow add_programs_to_channels to use Plex media types as well as Program --- dizqueTV/dizquetv.py | 38 +++++++++++++++++++++++++++++--- dizqueTV/models/channels.py | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index 981b47f..3b1df03 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -556,6 +556,38 @@ def channel_count(self) -> int: """ return len(self.channel_numbers) + @property + def highest_channel_number(self) -> int: + """ + Get the highest active channel number + + :return: Int number of the highest active channel + :rtype: int + """ + return max(self.channel_numbers) + + @property + def lowest_channel_number(self) -> int: + """ + Get the lowest active channel number + + :return: Int number of the lowest active channel + :rtype: int + """ + return min(self.channel_numbers) + + @property + def lowest_available_channel_number(self) -> int: + """ + Get the lowest channel number that doesn't currently exist + + :return: Int number of the lowest available channel + :rtype: int + """ + possible = range(1, self.highest_channel_number + 2) # between 1 and highest_channel_number + 1 + # find the lowest number of the differences in the sets + return min(set(possible) - set(self.channel_numbers)) + 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 @@ -1406,15 +1438,15 @@ def parse_custom_shows_and_non_custom_shows(self, items: list, non_custom_show_t return final_items def add_programs_to_channels(self, - programs: List[Program], + programs: List[Union[Program, CustomShow, Video, Movie, Episode, Track]], channels: List[Channel] = None, channel_numbers: List[int] = None, plex_server: PServer = None) -> bool: """ Add multiple programs to multiple channels - :param programs: List of Program objects - :type programs: List[Program] + :param programs: List of Program, CustomShow plexapi.video.Video, plexapi.video.Movie, plexapi.video.Episode or plexapi.audio.Track objects + :type programs: List[Union[Program, CustomShow, plexapi.video.Video, plexapi.video.Movie, plexapi.video.Episode, plexapi.audio.Track]] :param channels: List of Channel objects (optional) :type channels: List[Channel], optional :param channel_numbers: List of channel numbers diff --git a/dizqueTV/models/channels.py b/dizqueTV/models/channels.py index 19cb250..f8803bd 100644 --- a/dizqueTV/models/channels.py +++ b/dizqueTV/models/channels.py @@ -2,6 +2,8 @@ from typing import List, Union from plexapi.audio import Track +from plexapi.collection import Collection +from plexapi.playlist import Playlist from plexapi.server import PlexServer as PServer from plexapi.video import Video, Movie, Episode @@ -557,6 +559,48 @@ def add_programs(self, channel_data['duration'] += program.duration return self.update(**channel_data) + @decorators.check_for_dizque_instance + def add_collection(self, + collection: Collection, + plex_server: PServer) -> bool: + """ + Add a collection to this channel + + :param collection: PlexAPI Collection to add to this channel + :type collection: plexapi.collection.Collection + :param plex_server: plexapi.server.PlexServer object + :type plex_server: plexapi.server.PlexServer + :return: True if successful, False if unsuccessful (Channel reloads in place) + :rtype: bool + """ + if not collection: + raise MissingParametersError("You must provide a collection to add to the channel.") + items = collection.items() + if not items: + raise GeneralException("The collection you provided is empty.") + return self.add_programs(programs=items, plex_server=plex_server) + + @decorators.check_for_dizque_instance + def add_playlist(self, + playlist: Playlist, + plex_server: PServer) -> bool: + """ + Add a playlist to this channel + + :param playlist: PlexAPI Playlist to add to this channel + :type playlist: plexapi.playlist.Playlist + :param plex_server: plexapi.server.PlexServer object + :type plex_server: plexapi.server.PlexServer + :return: True if successful, False if unsuccessful (Channel reloads in place) + :rtype: bool + """ + if not playlist: + raise MissingParametersError("You must provide a playlist to add to the channel.") + items = playlist.items() + if not items: + raise GeneralException("The playlist you provided is empty.") + return self.add_programs(programs=items, plex_server=plex_server) + @decorators.check_for_dizque_instance def update_program(self, program: Program, From f12b06220b29ed5ce6aae617100453e48f33685d Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Wed, 16 Feb 2022 23:33:13 -0700 Subject: [PATCH 2/4] - Limit item types for add_collection and add_playlist functions --- dizqueTV/models/channels.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dizqueTV/models/channels.py b/dizqueTV/models/channels.py index f8803bd..15dc706 100644 --- a/dizqueTV/models/channels.py +++ b/dizqueTV/models/channels.py @@ -576,9 +576,13 @@ def add_collection(self, if not collection: raise MissingParametersError("You must provide a collection to add to the channel.") items = collection.items() - if not items: + final_items = [] + for item in items: + if type(item) in [Program, CustomShow, Video, Movie, Episode, Track]: + final_items.append(item) + if not final_items: raise GeneralException("The collection you provided is empty.") - return self.add_programs(programs=items, plex_server=plex_server) + return self.add_programs(programs=final_items, plex_server=plex_server) @decorators.check_for_dizque_instance def add_playlist(self, @@ -597,9 +601,13 @@ def add_playlist(self, if not playlist: raise MissingParametersError("You must provide a playlist to add to the channel.") items = playlist.items() - if not items: + final_items = [] + for item in items: + if type(item) in [Program, CustomShow, Video, Movie, Episode, Track]: + final_items.append(item) + if not final_items: raise GeneralException("The playlist you provided is empty.") - return self.add_programs(programs=items, plex_server=plex_server) + return self.add_programs(programs=final_items, plex_server=plex_server) @decorators.check_for_dizque_instance def update_program(self, From 47b47475ad38cb45276aecc19b968e55ff95dee3 Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Thu, 17 Feb 2022 09:55:44 -0700 Subject: [PATCH 3/4] - Preparing for more tests, using real Plex server if needed --- tests/test_dizquetv.py | 201 +++++++++++++++++++++-------------------- 1 file changed, 105 insertions(+), 96 deletions(-) diff --git a/tests/test_dizquetv.py b/tests/test_dizquetv.py index 3ddf923..27babee 100644 --- a/tests/test_dizquetv.py +++ b/tests/test_dizquetv.py @@ -1,101 +1,110 @@ from time import sleep +import pytest + import dizqueTV from tests.setup import client, plex_server, plex_server_as_dizquetv_server, fake_plex_server - -def test_dizquetv_server_details(): - details = client().dizquetv_server_details - assert details.server_version != '' - assert details.ffmpeg_version != '' - assert details.nodejs_version != '' - - -def test_dizquetv_version(): - version = client().dizquetv_version - assert version != '' - - -def test_ffmpeg_version(): - version = client().ffmpeg_version - assert version != '' - - -def test_nodejs_version(): - version = client().nodejs_version - assert version != '' - - -def test_ffmpeg_settings(): - settings = client().ffmpeg_settings - assert settings is not None - assert settings.configVersion != '' - - -def test_plex_settings(): - settings = client().plex_settings - assert settings is not None - assert settings.transcodeBitrate != '' - - -def test_xmltv_settings(): - settings = client().xmltv_settings - assert settings is not None - assert settings.file != '' - - -def test_hdhr_settings(): - settings = client().hdhr_settings - assert settings is not None - assert settings.tunerCount != '' - - -def test_add_plex_server(): - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert server is None - server = client().add_plex_server(**fake_plex_server) - assert type(server) == dizqueTV.PlexServer - - -def test_plex_servers(): - servers = client().plex_servers - assert type(servers) == list - - -def test_plex_server_status(): - status = client().plex_server_status(server_name=fake_plex_server['name']) - assert type(status) == bool - - -def test_plex_server_foreign_status(): - status = client().plex_server_foreign_status(server_name=fake_plex_server['name']) - assert type(status) == bool - - -def test_get_plex_server(): - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert type(server) in [dizqueTV.PlexServer, None] - - -def test_update_plex_server(): - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert server.arGuide is False - success = client().update_plex_server(server_name=fake_plex_server['name'], arGuide=True) - # request/update is successful, but throwing a timeout error, so success is False - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert server.arGuide is True - - -def test_delete_plex_server(): - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert server is not None - success = client().delete_plex_server(server_name=fake_plex_server['name']) - # request/update is successful, but throwing a timeout error, so success is False - sleep(1) - server = client().get_plex_server(server_name=fake_plex_server['name']) - assert server is None - - -def test_channel_count(): - count = client().channel_count - assert type(count) == int +REAL_PLEX_SERVER_ADDED = False + + +@pytest.fixture() +def use_real_plex(): + if not client().get_plex_server(plex_server().friendlyName): + yield client().add_plex_server_from_plexapi(plex_server()) + else: + yield client().get_plex_server(plex_server().friendlyName) + + +class TestGeneral: + def test_dizquetv_server_details(self): + details = client().dizquetv_server_details + assert details.server_version != '' + assert details.ffmpeg_version != '' + assert details.nodejs_version != '' + + def test_dizquetv_version(self): + version = client().dizquetv_version + assert version != '' + + def test_ffmpeg_version(self): + version = client().ffmpeg_version + assert version != '' + + def test_nodejs_version(self): + version = client().nodejs_version + assert version != '' + + def test_ffmpeg_settings(self): + settings = client().ffmpeg_settings + assert settings is not None + assert settings.configVersion != '' + + def test_plex_settings(self): + settings = client().plex_settings + assert settings is not None + assert settings.transcodeBitrate != '' + + def test_xmltv_settings(self): + settings = client().xmltv_settings + assert settings is not None + assert settings.file != '' + + def test_hdhr_settings(self): + settings = client().hdhr_settings + assert settings is not None + assert settings.tunerCount != '' + + def test_channel_count(self): + count = client().channel_count + assert type(count) == int + + +class TestWithFakePlex: + def test_add_plex_server(self): + # add a fake Plex server + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert server is None + server = client().add_plex_server(**fake_plex_server) + assert type(server) == dizqueTV.PlexServer + + def test_plex_servers(self): + servers = client().plex_servers + assert type(servers) == list + + def test_plex_server_status(self): + status = client().plex_server_status(server_name=fake_plex_server['name']) + assert type(status) == bool + + def test_plex_server_foreign_status(self): + status = client().plex_server_foreign_status(server_name=fake_plex_server['name']) + assert type(status) == bool + + def test_get_plex_server(self): + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert type(server) in [dizqueTV.PlexServer, None] + + def test_update_plex_server(self): + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert server.arGuide is False + success = client().update_plex_server(server_name=fake_plex_server['name'], arGuide=True) + # request/update is successful, but throwing a timeout error, so success is False + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert server.arGuide is True + + def test_delete_plex_server(self): + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert server is not None + success = client().delete_plex_server(server_name=fake_plex_server['name']) + # request/update is successful, but throwing a timeout error, so success is False + sleep(1) + server = client().get_plex_server(server_name=fake_plex_server['name']) + assert server is None + + +class TestWithRealPlex: + def test_use_real_plex(self, use_real_plex): + server = use_real_plex + assert server is not None + assert type(server) == dizqueTV.PlexServer + assert server.name == plex_server().friendlyName From 12aa9d0fe2841d28e58001122e948a45fddfa26e Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Fri, 8 Apr 2022 22:54:33 -0600 Subject: [PATCH 4/4] - Run channel JSON loading multithreaded to speed up channel loading process --- dizqueTV/dizquetv.py | 20 ++++++++++++++------ dizqueTV/helpers.py | 30 ++++++++++++++++++++++++++++++ tests/test_dizquetv.py | 4 ++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/dizqueTV/dizquetv.py b/dizqueTV/dizquetv.py index 3b1df03..e98574c 100644 --- a/dizqueTV/dizquetv.py +++ b/dizqueTV/dizquetv.py @@ -1,7 +1,8 @@ import json import logging +from concurrent.futures import ThreadPoolExecutor from datetime import datetime -from typing import List, Union +from typing import List, Union, Dict from xml.etree import ElementTree import m3u8 @@ -467,6 +468,10 @@ def delete_plex_server(self, server_name: str) -> bool: # Channels + def _get_channel_data(self, channel_number: int) -> Union[Dict, None]: + # large JSON may take longer, so bigger timeout + return self._get_json(endpoint=f'/channel/{channel_number}', timeout=5) + @property def channels(self) -> List[Channel]: """ @@ -475,14 +480,17 @@ def channels(self) -> List[Channel]: :return: List of Channel objects :rtype: List[Channel] """ - # temporary patch until /channels API is fixed. SLOW. + # temporary patch until /channels API is fixed. Runs concurrently to speed up. numbers = self.channel_numbers channels = [] - for number in numbers: - json_data = self._get_json(endpoint=f'/channel/{number}', timeout=5) - # large JSON may take longer, so bigger timeout + + channels_json_data = helpers._multithread(func=self._get_channel_data, elements=numbers, + element_param_name="channel_number") + + for json_data in channels_json_data: if json_data: channels.append(Channel(data=json_data, dizque_instance=self)) + return channels def get_channel(self, channel_number: int = None, channel_name: str = None) -> Union[Channel, None]: @@ -584,7 +592,7 @@ def lowest_available_channel_number(self) -> int: :return: Int number of the lowest available channel :rtype: int """ - possible = range(1, self.highest_channel_number + 2) # between 1 and highest_channel_number + 1 + possible = range(1, self.highest_channel_number + 2) # between 1 and highest_channel_number + 1 # find the lowest number of the differences in the sets return min(set(possible) - set(self.channel_numbers)) diff --git a/dizqueTV/helpers.py b/dizqueTV/helpers.py index b826663..8f23888 100644 --- a/dizqueTV/helpers.py +++ b/dizqueTV/helpers.py @@ -2,6 +2,7 @@ import json import os import random +from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED from datetime import datetime, timedelta from typing import List, Union, Tuple @@ -19,6 +20,35 @@ # Internal Helpers +def _multithread(func, elements: List, element_param_name: str, thread_count: int = 20, **kwargs) -> List: + """ + Multithread a function for elements in a list + + :param func: Function to be multithreaded + :type func: function + :param elements: List of elements to be multithreaded + :type elements: list + :param element_param_name: Name of the parameter to be passed to the function + :type element_param_name: str + :param thread_count: Number of threads to use + :type thread_count: int, optional + :param kwargs: Keyword arguments to be passed to the function + :type kwargs: dict, optional + :return: List of results from the function + :rtype: list + """ + thread_list = [] + pool = ThreadPoolExecutor(thread_count) + + for element in elements: + temp_kwargs = kwargs.copy() + temp_kwargs[element_param_name] = element + thread_list.append(pool.submit(func, **temp_kwargs)) + + wait(thread_list, return_when=ALL_COMPLETED) + return [t.result() for t in thread_list] + + def _combine_settings_add_new(new_settings_dict: dict, default_dict: dict, ignore_keys: List = None) -> dict: diff --git a/tests/test_dizquetv.py b/tests/test_dizquetv.py index 27babee..68bb301 100644 --- a/tests/test_dizquetv.py +++ b/tests/test_dizquetv.py @@ -59,6 +59,10 @@ def test_channel_count(self): count = client().channel_count assert type(count) == int + def test_channel_list(self): + channels = client().channels + assert type(channels) == list + class TestWithFakePlex: def test_add_plex_server(self):